1.什么是内存对齐?
以一个例子来说:
1 | struct test{ |
在 Inter i5 + Ubuntu 18.04 + gcc 7.5 上输出:
1 | size : 12 |
并不是想象中的应该是 1 + 4 + 1 = 6 btyes,这正是由于内存对齐而产生的结果。
2. 为什么会进行内存对齐
原因1-可移植性
部分基于 arm 架构的 CPU 并不能随意访问任意地址上的数据,只能访问特定地址上的数据。在非内存对齐的情况下,以上面为例,假设成员 a 存放在地址0x0处,则成员 i 存放在 0x1 处,此时这些 CPU 执行二进制代码访问成员 i 时就会崩溃。因此,为了提高代码的可移植性,需要进行内存对齐。
原因2-提高读取速度
CPU 从内存地址中读取数据需要消耗时间。如果没有内存对齐,CPU 读取成员 i ,第一次读取地址0x0~0x3的数据,第二次读取地址0x4~0x7的数据,然后将地址0x1~0x4的数据进行拼接从而获取成员 i 的值,这样 CPU 获取成员 i 的值需要读取两次内存并进行一次拼接,再需要大量从内存中读取数据时会显著影响程序的运行效率。
3. C语言进行内存对齐的规则
一般来说,CPU按块读取内存,在 32 位系统中每块大小位 4 字节,在 64 位系统中是 8 字节。这个块大小称为对齐数,可以通过关键字 #pragma pack(N) 来手动指定对齐数,例如#pragma pack(4)
整体对齐数
假设结构体成员中最大的类型占用的字节数为 m,则对齐单位
。结构体整体对齐后的字节数应为 的整数倍,不够应该在后面补占位字节。 成员对齐数
假设首个成员的相对偏移地址为 0,某个成员的类型占用字节数为 j,则该成员的相对偏移地址应当满足
的整数倍
例如,在Inter i5 + Ubuntu 18.04 + gcc 7.5 的环境下:
1 | // n = min(8,8) = 8 |
结构体所占用的大小为 24 字节。
当设置了 #pragma pack(4) 修改默认对齐数之后:
1 |
|
结构体所占用的大小为 20 字节。
在嵌套结构体的情况下,应先对最内层的结构体进行内存对齐,并将对齐后所占用的字节数作为该类型的大小进行计算。例如:
1 | // 占用8字节 |
结构体所占用的大小为 32 字节。
3.1 基于内存对齐规则减少内存浪费
让占用空间小的成员尽量集中在一起
例如上面的例子中,可以调整结构体成员的位置,使得该结构体所占用的内存空间最小:
1 | struct A{ |
这样调整结构体成员的顺序之后,该结构体占用仅为 16 字节,比之前 24 字节还是减少了挺多的。
4. 位域
有时数据的存储并不需要一个完整的字节,仅需要几个二进制位就可以表示。在这种情况下,C语言支持位域存储,对于一个变量声明其仅需要几个二进制位来存储;使用位域能够节约存储占用。
例如:
1 | struct pack |
对于位域,如果赋值超过其取值范围,则参照无符号/有符号整型变量的溢出规则;
4.1 位域的存储
结构体中位域的存储遵循以下规则(对于 GCC 编译器):
- 对于 GCC 编译器会尽量利用空闲的位对数据进行存储;
- 若位域之间定义有匿名位域成员,则匿名位域成员指定的空闲位不用于后续成员的数据存储;
- 特别地,如果定义的结构体中匿名成员占用的位数为0,则该匿名成员占用该变量所剩下的所有位
4.2 位域的对齐规则
- 首先按照存储原则计算相邻的位域所占用的总字节数,该区域与 1 进行地址对齐
- 其他成员变量以及整体的对齐规则如第 3 节所示
4.2.1 例子
1 |
|
整个结构体刚好占用 20 个字节,和 4 对齐,因此不需要补字节。
1 | struct A{ |
整个结构体刚好占用 24 个字节,和 4 对齐,因此不需要补字节。
5.取消内存对齐
虽然内存对齐可以加快读取内存的速度,但是在部分情况下需要取消默认的内存对齐,例如从 BMP 文件中读取 BMP 文件头时,需要让编译器取消内存对齐,防止其对结构体填充不必要的字节,使得结构体和文件中值不能刚好一一对应。
例1:__attribute__((packed))
1 | typedef struct tagBITMAPFILEHEADER |
表示该结构体使用最小的内存,编译器不会对其做对齐;这里是读BMP文件的文件头,不能让编译器填充额外字节。
例2:
1 | struct example { |
指示该结构体按 4 字节对齐
参考:
【C/C++】内存对齐 到底怎么回事? - 知乎 (zhihu.com)