关于内存对齐、位域的思考

什么是内存对齐?为什么需要它?

内存对齐(Memory Alignment)是计算机系统中数据在内存中存储的一种规则:​​数据在内存中的起始地址必须是其自身大小的整数倍​​。例如,一个 4 字节的整型变量(int),其起始地址必须是 4 的倍数(如地址 0x0000、0x0004、0x0008 等)。

而需要内存对齐主要基于以下三个原因:

  1. ​硬件访问效率​​:
    • CPU 通过内存总线从内存读取数据时,通常以固定大小的“块”为单位(例如 4 字节或 8 字节)。如果数据对齐,CPU 一次读取操作即可获取完整数据。
    • ​非对齐示例​​:假设一个 int 变量(4 字节)存储在地址 0x0001(非 4 的倍数),CPU 需要分两次读取:先读取 0x0000-0x0003(包含前 3 字节),再读取 0x0004-0x0007(包含最后 1 字节),最后拼接数据。这会显著降低性能。
  2. ​硬件兼容性​​:
    • 部分架构(如 ARM、MIPS)的 CPU 无法直接访问非对齐内存。尝试访问时会导致硬件异常(如“总线错误”)。对齐保证了代码的跨平台兼容性。
  3. ​缓存效率优化​​:
    • 现代 CPU 使用缓存行(Cache Line,通常 64 字节)预加载数据。对齐的数据更可能完整地位于单个缓存行中。若数据跨缓存行存储,会引发两次缓存访问,降低效率。

alignas 和 alignof

c++11 以后引入两个关键字 alignas 与 alignof

alignas 用于显式设置变量、类成员或类型的内存对齐要求;而 alignof 用于获取类型或变量的内存对齐要求。例如:

1
2
3
4
5
6
7
8
struct Test1 {};
struct alignas(4) Test2 {};

static_assert(sizeof(Test1) == 1);
static_assert(sizeof(Test2) == 4);

static_assert(alignof(Test1) == 1);
static_assert(alignof(Test2) == 4);

alignas 支持三种语法形式:

  • alignas(expression):expression 必须是计算结果为零的整数常量表达式,或者是对齐或扩展对齐的有效值(2 的倍数)。
  • alignas(type-id):等效于 alignas(alignof(type-id))
  • alignas(pack...):等效于应用于同一声明的多个 alignas 说明符,参数包的每个成员对应一个说明符,可以是类型参数包或常量参数包
📝 备注

注意:若指定的对齐值小于编译器默认对齐要求,部分编译器可能忽略该设置。

结构体字节对齐

结构体对齐的基本原则如下:

  1. 每个成员在其自身大小的整数倍地址上对齐
  2. 结构体本身的对齐方式等于其最大成员的对齐要求
  3. 结构体的大小是其对齐方式的整数倍(尾部可能填充)

举个🌰:

1
2
3
4
5
6
7
8
struct Student {
	char name[5];
	int num;
	short score;
};

cout << sizeof(Student) << '\n'; // 16
cout << alignof(Student);        // 4
成员大小对齐要求(alignment)
char[5]5 字节1 字节
int4 字节4 字节
short2 字节2 字节

结构体整体对齐通常等于 最大对齐成员的对齐要求max(1, 4, 2) = 4 字节。

其内存布局如下:

1
2
3
4
5
Byte Offset:  0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15
              +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Field:        | n0| n1| n2| n3| n4|   |   |   | num (4 bytes) | s0| s1|   |   |
              +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Meaning:      |      name[5]      |   padding |      num      | score |padding|

如果采用 __attribute__((__packed__)) (GCC/Clang 编译器提供的一个属性)进行“单字节对齐”,则会去除所有自动添加的 padding,每个成员从它前一个成员紧挨着的下一个字节开始

1
2
3
4
5
6
7
8
struct Student {
	char name[5];
	int num;
	short score;
} __attribute__((__packed__));

cout << sizeof(Student) << '\n'; // 11
cout << alignof(Student);        // 1
1
2
3
4
5
Byte Offset:  0   1   2   3   4   5   6   7   8   9   10
              +---+---+---+---+---+---+---+---+---+---+---+
Field:        | n0| n1| n2| n3| n4| m0| m1| m2| m3| s0| s1|
              +---+---+---+---+---+---+---+---+---+---+---+
Meaning:      |       name[5]     |       num     | score |

这种情况通常用于网络协议头、二进制文件结构、设备寄存器映射等。

位域

“位域“是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数,每个域有一个域名,允许在程序中按位域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

位域的对齐规则为:

  1. 位域的基础类型决定了分配单位,比如 unsigned int : 3 表示分配在 int 的机器字上(通常是 4 字节)
  2. 多个位域会尽量共享同一个机器字,但:
    • 如果一个字段放不下了,会拆到下一个对齐单元
    • 不同基础类型之间可能强制对齐
  3. 结构体大小和对齐也会根据最大基础类型来对齐

对于下面这个🌰:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct BitField {
	int a1 : 1;
	int a2 : 5;
	int a3 : 29;
	int a4 : 6;
	char a5 : 2;
	char a6;
};

cout << sizeof(BitField) << '\n';  // 12
cout << alignof(BitField) << '\n'; // 4
  1. 所有 int 位域(a1 ~ a4)以 int(4 字节 = 32 位)为分配单元。
  2. 多个 int 位域可以共用同一个 int 单元,如果剩余位数够用。
  3. 一旦放不下一个字段,就会换下一个 4 字节单元
  4. 如果换了基础类型(比如从 intchar),通常会进行类型对齐
  5. 非位域成员如 char a6 仍需按其类型对齐(通常是 1 字节,但结构体对齐会以最大成员为准)。

其内存布局如下:

1
2
3
4
Byte Offset:   0       1       2       3       4       5       6       7       8       9      10     11
              +---------------------------------------------------------------------------------------+
              |   a1:1, a2:5, a3前26位,共32位   |   a3后3位, a4:6, 剩余未用 bits  |a5:2...| a6:8 |  pad  |
              +---------------------------------------------------------------------------------------+
使用 Hugo 构建
主题 StackJimmy 设计