字节对齐的概念
伴随计算机技术的发展,CPU的数据位宽经历了16bit,32bit,64bit,以32bit CPU访问4字节内存为例,4字节对齐的存取速度是最快的。在非对齐的情况下,CPU需要分解成两次32bit操作(大部分平台下依然只是一条指令)
不同体系结构的CPU对非对齐访问操作的支持情况也不相同。
字节对齐的问题
Q:sizeof(struct x)是多少
struct X{int a; long b; char c;};
A:这个问题的答案首先取决于目标程序是32bit还是64bit的系统;其次还取决于编译选项是否有字节对齐选项,-fpack-struct;
例如,在32bit,无对齐选项下的结果是12;
32bit,-fpack-struct=1选项的情况下,结果是9;
64bit LP64模式下,无对齐选项,结果是24
64bit LP64模式下,-fpack-struct=1情况下,结果是13
64bit LLP64模式下,无选项,结果是12
64bit LLP模式下,含-fpack-struct=1选项,结果是9
LP64和LLP64:
LP64和LLP64是不同的编译器数据模型。LP64表示long long,long 和pointer是64位,int是32位,LLP64表示long long和pointer是64位,int和long是32位。
各种数据类型的汇总如下:
Data type | ILP32 | LP32 | ILP64 | LP64 | LLP64 |
宏定义 | - | - | - | __LP64__ | __LLP64__ |
平台 | win32/unix和类unix | win16 | unix和类unix | win64 | |
char | 8 | 8 | 8 | 8 | 8 |
short | 16 | 16 | 16 | 16 | 16 |
int | 32 | 32 | 64 | 32 | 32 |
long | 32 | 32 | 64 | 64 | 32 |
long long | 64 | 64 | 64 | 64 | 64 |
pointer | 32 | 32 | 64 | 64 | 64 |
字节对齐规则
a. 在C语言默认规则中,基本数据类型的对齐值与自身大小一致。比如对于int,float类型,其自身对齐值为4字节,对于double类型,其自身对齐值为8字节。
b. C语言默认规则中,聚合数据类型中每个成员都需要满足自身的对齐要求,且聚合类型整体的对齐值取决于成员中自身对齐值最大的值,聚合类型整体大小为对齐值的整数倍。聚合类型包括结构体/联合体/类等。
c.C的对齐规则与硬件平台,32位还是64位系统无关,属于语言规范。其目的是实现对每个元素(包括聚合类型内部的元素)访问都按照对齐的高效率方式。
理解C语言字节对齐规则的关键:
规则的目的是让出现在代码中的每一个基本类型数据都能够自对齐,从而提高访问效率,无论这个基本类型数据是出现在数组中,结构中,联合体中或者结构嵌套定义中等等。
比如为了结构数组中的元素自对齐,就导致了结构大小必须也是该对齐值的倍数,否则会产生矛盾。
编译器中的字节对齐规则:
gcc中编译选项-fpack-struct[=n]或预编译指令#pragma pack(n),可以限定对齐值的上限;
gcc中变量数据__attribute__((aligned(n))) 可以约定对齐值的下限。
C11 对齐关键字:_Alignas 、_Alignof
_Alignas含义和在C99中使用GUN attribute ((aligned(n))) 或者 #program pack(n)一样,可以用于指定对齐。使用_Alignas 说明符指定变量或类型的对齐值,必须为2的非负整数幂,且不小于其自身对齐值。
Alignof用于获取获取类型或变量的对齐值。
例如:
size_t typedef struct { int a; double b; } S; // alignof(S) == 8
_Alignas的使用举例:
_Alignas(2) char data; //_Alignas指定的对齐值2大于char自身对其值,是正确的声明方式。
_Alignas(3) char data; //3不是2的非负整数次幂,错误
字节对齐举例
以下举例都是32位ARM平台
1.
typedef struct {
char c1;
short s2;
char c3;
}S;
由于结构体中对齐要求最大的值是short s2,2字节对齐,因此c1后补1个字节,保证S2是2字节对齐,C3后补1个字节,保证结构体整体是2字节对齐。
2.
typedef struct {
char c1;
long l2;
char c3;
} S;
由于结构体中对齐要求最大的值是long l2,4字节对齐,因此c1后补3个字节,保证l2是4字节对齐,C3后补3个字节,保证结构体整体是4字节对齐。
typedef struct {
char s[2];
U64 type;
} S;
S g_var; /*变量起点8字节对齐,大小16字节*/
#pragma pack(1)
typedef struct {
char s[2];
U64 type;
} S;
S g_var; /*变量起点无对齐要求,大小10字节*/
#pragma pack(4)
typedef struct {
char s[2];
} S;
#pragma pack()
/*-fpack-struct = 4选项*/
typedef struct {
char s[2];
} S;
上面两个结构对齐值都是1,大小为2字节。
注意:#pragma pack(n),-fpack-struct = n控制对齐值上限为n,即比较自对齐值与n值,取较小值。
字节对齐的平台差异
普通内存的读取与存储访问下,不同平台的对齐要求如下
a. 不同体系结构下对非对齐访问的支持情况不同;“不支持”意味着相关操作会异常
b. power PC,intel系列CPU均支持非对齐访问,即同样一条指令既能用于对齐内存访问,也支持非对齐。
c. ARM平台支持有限的非对齐访问
1) ARM下需要设置特别的控制寄存器才能支持非对齐访问
2)ARMv7下4字节对齐访问指令支持非对齐,8字节访问指令不支持;多寄存器存取指令不支持
3) ARM V8下各种宽度内存访问指令均支持非对齐
d. mips平台通过一条指令完成对齐内存的访问,通过两条指令完成非对齐内存访问,编译器可自动选择配套指令。
非普通内存的读取与存储访问下,对齐要求比较特殊
a. 用于原子操作/锁等互斥内存读写的地址,不同平台下均要求对齐。X86下出于兼容性考虑可能例外,但非对齐的原子操作非常影响性能
b. 对于嵌入式系统外设寄存器空间的内存访问,也有对齐要求,比如ARM外设空间地址的MMU都是device类型,ARM 4字节的store/load指令对device空间访问时都要求4字节对齐。
一些建议
C语言的默认对齐规则下不会产生非对齐访问
a. 静态数据定义时编译器分配满足对齐条件的地址
b. 每一层函数对栈的使用都是8字节的倍数,所以栈中的结构也都可以满足8字节以内的对齐要求。
c. C库中的malloc也至少按照8字节对齐返回内存地址
但是开发人员在实现代码时,例如通过强转某片内存为结构体使用,就需要特别留意对齐的要求。
在编写代码时,关于字节对齐,我们需要注意以下几点:
a. 非对齐访问会影响性能(在部分内存敏感的场景下,由于增加了一条指令会导致codesize增加),甚至部分硬件平台下会产生异常
b. 定义结构时需要利用对齐知识留意元素排序,减少内存浪费
下一篇:
深入理解C语言(二)——比特序和字节序