先给几个数据:
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,但是已经跟原始大小差不多了。
还能进一步压榨么?
如果要想进一步压榨,就要从变量大小上打主意了。举个例子,这里的red
,blue
,green
也许不需要使用int类型,x
和y
也是同理,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的内容,所以不能直接移动单个字节,这就导致了速度的下降。