先给几个数据:

cout << "sizeof char is " << sizeof( char ) << endl;
// 1
cout << "sizeof short is " << sizeof( short ) << endl;
// 2
cout << "sizeof int is " << sizeof( int ) << endl;
// 4
cout << "sizeof bool is " << sizeof( bool ) << endl;
// 1
cout << "sizeof double is " << sizeof( double ) << endl;
// 8

好,那么问题来了,

struct Spot
{
    int x;
    int y;
    bool visible;
    int red;
    int blue;
    int green;
    double alpha;
    bool cleaned;
};
...
Spot spot;
cout << "sizeof Spot is " << sizeof( spot ) << endl;

输出的结果是多少?

这个问题对于写过C/C++的人来说,有点侮辱智商……好吧,不逗你玩了,直接进入正题。

输出的结果肯定不会是29(2 * 4 + 1 + 3 * 4 + 8)啦。都是Data structure alignment惹的祸。

Data structure alignment是个复杂的概念,简单来说,就是因为CPU访问内存时是成块成块读取数据的,所以编译器为了让CPU访问的时候更加方便些(同时也使得程序更加高效些),会将变量的内存地址移动到2的N次幂上。而为此空出来的空间叫做padding,在计算结构体总大小的时候,也得考虑这些padding。

那么怎么计算结构体的实际大小呢?由于C/C++是一门跟硬件密切相关的“底层”语言,这要看具体的硬件和相关的编译器实现了。这里给出一个可行的方法:http://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/ 。简单说,假设CPU每次读取内存都是读4个字节,那么要把变量内存地址移到4的倍数上。不过在64位系统中,CPU每次可以读取8个字节,所以8个字节的double变量地址要位于8的倍数上。一个个变量算下来,最后就会得到结构体的实际大小了。

一个明显的推断是,如果改变结构体中声明的变量的位置,就能减少padding占用的空间。

记得有一个故事说,有个人想要填满一个杯子,他首先装上小石头,再装上一些沙子,最后倒入水,这时候杯子才真正满了。要想充分利用一个容器的空间,就要先装上体积较大的物体,然后依次装上体积稍微小的的物体。再来看下原来的声明:

struct Spot
{
    int x;
    int y;
    bool visible;
    int red;
    int blue;
    int green;
    double alpha;
    bool cleaned;
};

把各个变量按照从大到小重新排列,得到:

struct Spot
{
    double alpha;
    int x;
    int y;    
    int red;
    int blue;
    int green;
    bool visible;
    bool cleaned;
};

此时spot的大小仅为32byte,虽然还是有padding,但是已经跟原始大小差不多了。

还能进一步压榨么?

如果要想进一步压榨,就要从变量大小上打主意了。举个例子,这里的redbluegreen也许不需要使用int类型,xy也是同理,alpha可以使用float来代替。假如这些都成立,那么可以精简为:

struct Spot
{
    float alpha;
    unsigned short x;
    unsigned short y;    
    unsigned short red;
    unsigned short blue;
    unsigned short green;
    bool visible;
    bool cleaned;
};

对于热衷于压榨每一点资源的人来说,还是有一点不满意的。也许red,blue,green也就是0到255的范围,不过是2的8次方。那么用bool类型来代替呢?这却有一个问题,为什么一个表示颜色成分的变量是bool类型呢,这个除了大小,跟bool类型没有半点关系。类型语义上说不过去啊。

C++有一个语言特性解决了这个问题:Bit field

我们可以在声明变量的时候,给对应的变量指定一个bit field大小(单位是bit),改变该变量默认占用的内存大小:

struct Spot
{
    float alpha;
    unsigned short x;
    unsigned short y;    
    unsigned short red : 8;
    unsigned short blue : 8;
    unsigned short green : 8;
    bool visible;
    bool cleaned;
};

注意不能对bit field使用sizeof,编译器会报错。

嗯,虽然由于Data structure alignment的原因,spot的整体大小并没有改变,不过相关变量所占用的内存的确减少了。而且这种减少,并没有导致变量访问速度上的影响。
但是如果更进一步,把visible等bool类型变量变成大小为1 bit的bit field(我知道肯定有人迫不及待地想这么做),就会影响变量访问速度,因为现在bool变量已经不能凑整了。实验证明,速度有慢10%左右。

不如看下具体生成的汇编代码:

// size.cpp (without bit field)
...
spot.blue = 3;
...

然后……

$ g++ -g ./size.cpp
$ objdump -S --disassemble ./a.out > before
$ g++ -g ./size_with_bit_field.cpp
$ objdump -S --disassemble ./a.out > after
$ vimdiff before after

你会看到使用bit field之后,读取blue的指令是移动一个字节,而且取址的位置也不一样了。

换把bool类型变成bit field试试,这时候会发现,每次在访问bit field时,都会额外加多两条指令。因为这时候我们只需要取一个bit的内容,所以不能直接移动单个字节,这就导致了速度的下降。


spacewander
5.6k 声望1.5k 粉丝

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.