计算机课程指引人们避开微观的优化而去寻找更优的算法。 硬件价格的下降也使挤压内存占用变得没
有必要。
但该技术仍在重要的情况下有用武之地,而且只要内存有限制,就会有用。 这篇文章的目的是使C程
序员重新发现该技术,使他们能专注于更重要的事情。
1. 对齐的要求
首先要理解的是,在现代处理器上,C编译器在内存里存放基本数据类型时是受限的:以最快存取速度
为目标。
在X86或ARM上,基本数据类型并不是存放在任意内存地址上的。 每种类型除了char都有对齐要求
(alignment requirement); char类型可以开始于任何地址,但2字节的short类型必须存放在偶数地址
上,4字节的整型或浮点型必须放在能被4整除的位置上,而8字节的long或double型必须放在能被8
整除的地址上。有符号或无符号没有差别。
用术语来讲就是,基本C类型在X86和ARM上都是自对齐的(self-aligned)。指针,不管是32位(4字
节)还是64位(8字节)也是自对齐的。
自对齐能存取得更快是因为它能用一条指令来存取该类型数据。 另一方面,如果没有对齐限制,代码
可能会在跨机器字边界存取的时候使用两条以上的指令。 字符是特殊情况: 不管它在们在机器字的哪
个位置,存取代价都是一样的。所以它们没有对齐要求。
在现代处理器上,是因为在有些更老的处理器上,强迫你的C代码违反对齐限制(比如,把一个奇数地
址转换为int指针并试图使用它)不仅会让你的代码变慢,还会造成非法指令异常。 比如在Sun SPARC
芯片上就是这样。 事实上,只要有足够的决心和正确的硬件标志(e18),你也可以在X86上触发该异常
。
自对齐还不是唯一的规则。 历史上,有些处理器(特别是那些没有barrel shifters的)有更严格的规
则。如果你在做嵌入式系统,你可能撞到这些暗礁。要有心理准备。
有时你可以让编译器不遵守处理器的正常对齐规则,一般是使用pragma,比如 #pragma pack。 请不
要随意使用,因为它会生成开销更大、更慢的代码。 通过使用我介绍的技术,你可以节省同样、甚至
更多的内存。
使用#pragma pack的唯一合理理由是,你需要C数据分布完全匹配某些硬件或协议,比如一个经过内存
映射的物理端口,则不违反对齐规则就无法做下去。 如果你处在那种情况,而不理解本文的内容,你
会遇到大麻烦。
2. 填充(padding)
现在我们来看一个简单的例子,变量在内存中的分布。
char *p;
char c;
int x;
如果你不知道数据对齐,你可能会假定这三个变量在内存里占用连续的字节。 即,在32位机器上4字节
的指针后面会紧跟1字节的char,而它后面会紧跟4字节的int。在64位机器上,唯一的差别是指针是8字
节的。
而实际情况是这样的(在x86或ARM或任何自对齐的机器上):p 存储在4字节或8字节对齐的位置上(由机
器的字长决定)。 这是指针对齐-可能的最严格的情况。
c的存储紧跟着p。但x的4字节对齐要求造成一个缺口,就好像有第四个变量插入其中:
char *p; /* 4 or 8 bytes */
char c; /* 1 byte */
char pad[3]; /* 3 bytes */
int x; /* 4 bytes */
pad[3] 数组表示有3个字节浪费了。 老式的说法是“slop(溢出)”。
比较如果x 是2字节的short会怎样:
char *p;
char c;
short x;
在这种情况下,实际的内存分布是这样的:
char *p; /* 4 or 8 bytes */
char c; /* 1 byte */
char pad[1]; /* 1 byte */
short x; /* 2 bytes */
另一方面,如果是在64位机上,x 是一个long:
char *p;
char c;
long x;
我们会得到:
char *p; /* 8 bytes