c语言的结构体需要内存对齐,其实不止是结构体,为减少内存访问次数,任何内存对象都需要对齐。首先复习几个概念:字节(8bit)、字(16bit),双字(32bit)。下文分8位处理器、16位处理器和32位处理器分别说明内存对齐的原理。
8bit处理器
内存使用字节(8bit)编址,就是说一个确切的内存地址指向内存中的一个字节。如下图所示,地址0002指向从0起偏移为2的内存单元。早期CPU的数据总线也是8bit的,也就是每次只能传输一个字节的内容。所以读取一个字节需要一次内存访问,读取一个字需要两次内存访问,读取一个双字需要四次内存访问,并不存在对齐的要求。如果CPU要读取地址为0004的字,实际上它需要分两次读取地址为0004和0005两块内存单元。
+--------+
| 0005 |
|--------|
| 0004 |
|--------|
| 0003 |
|--------|
| 0002 | +--------------+
0002 --> |--------| | |
| 0001 | | CPU |
|--------| | |
| 0000 | +--+--------+--+
+--------+ | |
| | |<-8bit->|
|<-8bit->| | |
| +-----------+ |
+-----------------------------+
16bit处理器
无论对于什么样的处理器,内存都是字节编址的,每一个确切的内存地址指向一个字节的规则没有改变。对于16bit的处理器,其内存数据总线的带宽为16bit,即一次可以读取16bit的内容。对应的其内存采用了寄偶排列,数据总线的低8bit连接偶数字节,高8bit连接奇数字节。如果需要读取一个地址为0002的字节,数据总线传递地址为0002和0003的两个字节到CPU,CPU忽略不需要的字节即可。如果需要读取地址为0004的一个字只需访问一次内存即可将0004和0005两个字节的数据传递到CPU。如果要读取地址为0008的一个双字,则需两次内存访问。试想如果要访问地址为0005的一个字要如何操作?由于地址总线的低8bit连接奇地址内存,高8bit连接偶地址内存,所以CPU会发起两次内存操作,一次读取0004和0005两个字节,一次读取0006和0007两个字节,然后将0005和0006两个字节拼接起来完成这次操作。同理,如果要读取一个起始地址为奇数的双字需要三次内存访问然后完成数据的读取。不过对于x86这样的复杂指令计算机,CPU内部会自己完成数据的拼接。综上所述对于数据总线为16bit的处理器,数据的起始地址位2字节对齐时能够保证访问内存的次数最少。
+--------+ +--------+
| 0011 | | 0010 |
|--------| |--------|
| 0009 | | 0008 |
|--------| |--------|
| 0007 | | 0006 |
|--------| |--------|
| 0005 | | 0004 | +---------------+
|--------| |--------| | |
| 0003 | | 0002 | | CPU |
|--------| |--------| | |
| 0001 | | 0000 | +--+---------+--+
+--------+ +--------+ | |
| | |<-16bit->|
|<------16bit------>| | |
| +----------+ |
+----------------------------------------+
32bit和64bit处理器
对于32和64bit处理器,同样在内存地址为4字节、8字节对齐时访问数据所需的内存读取次数最少。C/C++语言的结构体、函数栈中的临时变量等都会按照处理器的数据总线位宽进行地址对齐。而一般情况下编译器会为我们完成数据的对齐操作,用户无需操心。
数据结构中的对齐
32bit的CPU需要数据的地址按照4字节对齐,对于结构体一般编译器会保证结构体的首地址为4字节对齐,所以对于结构体的成员编译器也会添加填充以使各成员满足对齐的要求,比如如下结构体,编译器会加入如下填充。假设数据结构的首地址为2000,访问var1只需读取起始地址位2000的一个双字,然后保留低字节的16bit丢弃高字节的16bit即可;读取var2时也读取地址为2000的一个双字,然后丢弃低位16字节和最高位的8字节即可获取var2的值;对于var3只需读取一次地址为2004的内存即可取回var3的值。三个成员变量都可以通过一次内存访问来获取。所以结构体字节对齐的大概原则是:对于比CPU数据总线位宽小的成员自然对齐即可,对于大于等于数据总线位宽的成员要保证首地址4字节对齐。在没有特殊对齐要求的情况下,将长度相同和相近的成员放在一起就可以基本保证结构体最小(由于C语言规范规定编译器不能改变结构体中各成员的排列顺序,所以需要程序员自己注意结构体中各成员的排列顺序)。
填充前:
struct example {
short var1;
char var2;
int var3;
}
填充后:
struct example {
short var1;
char var2;
char __padding;
int var3;
}