可变长度数据结构
固定长度的数据结构很简单,大家每天都在用。
可变长度数据结构,都可以通过内嵌对象的形式,转化成固定长度的数据结构,大家每天也都在用,例如:
1 2 3 4 5 6 |
struct person { int id; string name; string address; }; |
每个 person 对象的长度是固定的,但是,其内嵌的 name 和 address 是变长的。从而,整个对象占据的总空间也是变长的。
但是,将这样的的对象平坦化,使之只占据一块连续空间,使用的人很少,因为在绝大多数情况下,很少有人思考这个问题,并且,大多数问题已经使用内嵌数据结构解决掉了。
然而,如果内存很紧张,或者需要处理得数据量非常大,这种方式浪费的内存就太多了。假定我们现在创建了一个 map<int, person> ,在gcc4.1 的 64位环境中,按8字节对齐,这个 map 总共会占用多少内存呢?
- sizeof(map) = 48
- sizeof(string) = 8
- sizeof(person) = 24
- sizeof(person_node) = alignto(24 + 32(rbtree node overhead), 8) = 56
- sizeof(string_node) = 8*3(refcount, size_endptr, capacity_endptr)
- memsizeof(string) = sizeof(string_node) + alignto(strlen + 1, 8)
- memsizeof(person_node) = sizeof(person_node) + memsizeof(name) + memsizeof(address)
如果 avg(name) = 10, avg(address) = 20
实际占用的空间,大约还要再加上4:avg(name) = 14, avg(address) = 24
那么 memsizeof(person_node) = 56 + 24*2 + 14 + 24 = 142
那么该map 实际占用空间(gcc 的每个 map 还有一个虚结点:32个字节):
48+32+n*142 = 80+n*142
如果使用一种理想的变长数据结构,再加上红黑树的优化(none virtual node, compressed color, no parentptr),需要多少内存呢?
- sizeof(map) = 16
- memsizeof(person_node) = alignto(16(treenode) + 4(id) + 2*1(strlen) + 10(name) + 20(address) = 52, 8) = 56
memsizeof(map) = 16 + 56*n
内存一下节省了 60% 还多,也就是说,如果内存大小已经固定,可以装入 2.5 倍的数据。
- 如果用来做集群缓存,可以节省50%的机器(系统也要占一些内存)。
- 如果有5.6G的内存可用,就可以装入1亿条数据。
- 并且,在节省了60%内存的同时,还有另外一种好处:提高cache命中率,如果3个字段访问的频率相同,cpu的 cache miss 会降低3倍。
- 可以预见,因为cache miss降低,map.find 速度会大幅提高。
这个可变数据结构可以这样设计:
1 2 3 4 5 6 7 8 9 10 11 |
struct person2 { int id; unsigned char nName, nAddress; char data[1]; // not terminated with '/0' const char* getName() const { return data; } const char* getAddress() const { return data + nName; } int getSize() const { return offsetof(person, data) + nName + nAddress; } }; |
更复杂的情况:
- 如果nName 和 nAddress 要大于 255 呢?——把 unsigned char 改成 unsigned short, 甚至 unsigned int
- 如果person的字段很多,例如有20个字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
struct person3 { int id; string name; string address; string ex1; string ex2; string ex3; string ex4; string ex5; // more... // var data structure: // unsigned char nName, nAddress, nEx1, nEx2; // const char* getEx1() const { return data + nName + nAddress; } // const char* getEx2() const { return data + nName + nAddress + nEx1; } // const char* getEx3() const { return data + nName + nAddress + nEx1 + nEx2; } // const char* getEx4() const { return data + nName + nAddress + nEx1 + nEx2 + nEx3; } // const char* getEx5() const { return data + nName + nAddress + nEx1 + nEx2 + nEx3 + nEx4; } // more... }; |
这就不光是写代码的复杂了,运行时字段访问的性能也成问题!性能问题可以用另外一种方式——使用直接定位——来解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
struct person4 { int id; string name; string address; string ex1; string ex2; string ex3; string ex4; string ex5; // more... // var data structure: // unsigned short pAddress, pEx1, pEx2, pEx3, pEx4, pEor; // char data[1]; // const char* getName() const { return data + 0; } // const char* getAddress() const { return data + pAddress; } // const char* getEx1() const { return data + pEx1; } // const char* getEx2() const { return data + pEx2; } // const char* getEx3() const { return data + pEx3; } // const char* getEx4() const { return data + pEx4; } // const char* getEx5() const { return data + pEx5; } // more... // int sizeEx1() const { return pEx2 - pEx1; } // int sizeEx2() const { return pEx3 - pEx2; } // int sizeEx3() const { return pEx4 - pEx3; } // int sizeEx4() const { return pEx5 - pEx4; } // int sizeEx5() const { return pEor - pEx5; } }; |
如果需要变长数据的数组,怎么办?简单,offset array + data byte array,具体实现方式与 person4 类似,只不过 offset array 元素需要使用更宽的整数。
使用 febird.gold_hash_map/hash_strmap, 可以占用内存更少,并且速度更快