固定長度的數據結構很簡單,大家每天都在用。

可變長度數據結構,都可以通過內嵌對象的形式,轉化成固定長度的數據結構,大家每天也都在用,例如:

struct person
{
    int    id;
    string name;
    string address;
};

 

每個 person 對象的長度是固定的,但是,其內嵌的 name 和 address 是變長的。從而,整個對象佔據的總空間也是變長的。

 

但是,將這樣的的對象平坦化,使之只佔據一塊連續空間,使用的人很少,因為在絕大多數情況下,很少有人思考這個問題,並且,大多數問題已經使用內嵌數據結構解決掉了。

 

然而,如果內存很緊張,或者需要處理得數據量非常大,這種方式浪費的內存就太多了。假定我們現在創建了一個 map<int, person> ,在gcc4.1 的 64位環境中,按8字節對齊,這個 map 總共會佔用多少內存呢?

1. sizeof(map) = 48
2. sizeof(string) = 8
3. sizeof(person) = 24
4. sizeof(person_node) = alignto(24 + 32(rbtree node overhead), 8) = 56
5. sizeof(string_node) = 8*3(refcount, size_endptr, capacity_endptr)
6. memsizeof(string) = sizeof(string_node) + alignto(strlen + 1, 8)
7. 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),需要多少內存呢?

    1. sizeof(map) = 16
    2. 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 倍的數據。

    1. 如果用來做集羣緩存,可以節省50%的機器(系統也要佔一些內存)。
    2. 如果有5.6G的內存可用,就可以裝入1億條數據。
    3. 並且,在節省了60%內存的同時,還有另外一種好處:提高cache命中率,如果3個字段訪問的頻率相同,cpu的 cache miss 會降低3倍。
    4. 可以預見,因為cache miss降低,map.find 速度會大幅提高。

     

    這個可變數據結構可以這樣設計:

    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; }
    };

     

    更復雜的情況:

    1. 如果nName 和 nAddress 要大於 255 呢?——把 unsigned char 改成 unsigned short, 甚至 unsigned int
    2. 如果person的字段很多,例如有20個字段:

    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...
    };

     

    這就不光是寫代碼的複雜了,運行時字段訪問的性能也成問題!性能問題可以用另外一種方式——使用直接定位——來解決。

     

    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 元素需要使用更寬的整數。