参考

C/C++内存对齐详解 | zhihu

【到底为什么要内存对齐?】| bilibili

为什么要进行内存对齐

尽管内存是以字节为单位进行划分的, 但是大部分处理器并不是按字节块来存取内存的. 它一般会以 2字节, 4字节, 8字节, 16字节甚至32字节为单位来存取内存, 我们将上述这些存取单位称为内存存取粒度.

内存存取粒度的大小取决于存储字长寻址方式. 存储字长即存储体中一个存储单元所包含的存储元数量, 寻址方式通常包括: 1.按字节寻址, 2.按字寻址, 3.按半字寻址, 4.按双字寻址. (一个 word 的大小取决于存储字长)

现在考虑4字节存取粒度的处理器取int类型变量(32位系统), 该处理器只能从地址为4的倍数的内存开始读取数据.

假如没有内存对齐机制, 数据可以任意存放, 现在一个int变量存放在从地址1开始的连续4个字节地址中, 该处理器去取数据时, 要先从0地址开始读取第一个4字节块, 剔除不想要的字节(0地址), 然后从地址4开始读取下一个4字节块, 同样剔除不要的数据(5,6,7地址), 最后留下的两块数据合并放入寄存器. 相比于将int变量存放在从地址0开始的连续4字节地址中, 这种不对齐的读取方式效率很低.

对齐规则

基本类型 (如int, char, float): 编译器通常会对齐这些基本类型的变量. 例如,在一个32位系统上, int类型通常会对齐到 4 字节边界, 而 char 类型由于大小为 1 字节, 不需要额外对齐.

结构体 (struct) 和类 (class): 结构体和类中的成员变量通常会进行对齐. 为了使结构体中每个成员变量都按其类型的要求对齐, 编译器可能会在成员变量之间插入一些填充字节, 称为 “填充” 或 “空隙”.

结构体对齐
编译器为结构体的每个成员按照其自然边界 (alignment) 分配空间. 各成员按照它们被声明的顺序在内存中顺序存储, 第一个成员的地址和整个结构的地址相同, 即第一个数据成员的 offset = 0.

基本数据类型自身对齐值
char 型数据自身对齐值为 1 字节, short型数据为 2 字节, int/float 型为 4 字节, double型为 8 字节.

结构体的自身对齐值
结构体的自身对齐值为其成员中自身对齐值最大的值. 例如一个结构体包含 char, int, double, 那么这个结构体的自身对齐值为 max{sizeof(char), sizeof(int), sizeof(double)} = 8 bytes.

指定对齐值
#pragma pack (value) 时的指定对齐值 value, 默认是4.

基本数据类型/结构体的有效对齐值
自身对齐值和指定对齐值中较小者, 即有效对齐值 = min{自身对齐值, 当前指定的pack值}。

使用 #pragma pack (value) 指定对齐值, 其实是指定了数据结构的最大有效对齐值.

有效对齐值 N 是最终用来决定数据存放地址方式的值. ==“有效对齐 N” / “对齐在 N 上”, 表示即该数据的 “起始存放地址 % N == 0”==

结构体的成员变量要对齐存放: 1.结构体成员变量占用总长度为结构体有效对齐值的整数倍, 即结构体本身也要根据自身的有效对齐值 “圆整”, 结构体变量的大小要为结构体自身有效对齐值的整数倍; 2.结构体变量的起始地址要为结构体有效对齐值的整数倍.

示例, 假设 pack 使用默认值 4

1
2
3
4
5
6
7
8
9
struct A{
int i; // offset = 0
char c; // offset = 4
short s; // offset = 6
};
struct A a1; // &a1 % 4 == 0

// 0 1 2 3 4 5 6 7
// |i|i|i|i|c|0|s|s|

对于结构体 A 和结构体变量 a1. sizeof(a1) = 8 bytes, 结构体本身占用 8 字节的空间, 自身对齐值为 max{4, 1, 2} = 4, 有效对齐值为 min{4, pack} = 4, 结构体变量实际占用空间恰好为 4 的倍数

  • sizeof(a1.i) = 4 bytes, 第一个成员占用 4 字节的空间, 自身对齐值为 4, 有效对齐值为 min{4, pack} = 4, offset = 0;
  • sizeof(a1.c) = 1 byte, 第二个成员占用 1 字节的空间, 自身对齐值为 1, 有效对齐值为 min{1, pack} = 1, offset = 4;
  • sizeof(a1.s) = 2 bytes, 第三个成员占用 2 字节的空间, 自身对齐值为 2, 有效对齐值为 min{2, pack} = 2, offset = 6;

其中 char 类型由于自身对齐值为 1 字节, 所以不需要额外进行对齐, 即它的起始地址可以为任意地址. short 类型的自身对齐值为 2 字节, 它的起始地址需要为 2 的倍数, 所以这里在 a1.c 之后填充了 1 个字节, 使得 a1.s 的起始地址为 2 的倍数.

1
2
3
4
5
6
7
8
9
struct B{
char c; // offset = 0
int i; // offset = 4
short s; // offset = 8
};
struct B b1; // &b1 % 4 == 0

// 0 1 2 3 4 5 6 7 8 9 a b
// |c|0|0|0|i|i|i|i|s|s|0|0|

对于结构体 B 和结构体变量 b1. sizeof(a1) = 12 bytes, 结构体本身占用 12 字节的空间, 自身对齐值为 max{1, 4, 2} = 4, 有效对齐值为 min{4, 4} = 4, 为保证结构实际占用空间为 4 的倍数, 需要在 short 后面补 2 个字节.

  • sizeof(a1.c) = 1 bytes, 第一个成员占用 1 字节的空间, 自身对齐值为 1, 有效对齐值为 min{1, pack} = 1, offset = 0;
  • sizeof(a1.i) = 4 bytes, 第二个成员占用 4 字节的空间, 自身对齐值为 4, 有效对齐值为 min{4, pack} = 4, offset = 4;
  • sizeof(a1.s) = 2 bytes, 第三个成员占用 2 字节的空间, 自身对齐值为 2, 有效对齐值为 min{2, pack} = 2, offset = 8;

其中由于 b1.i 为 int 类型, 需要进行 4 字节对齐, 所以在 b1.c 之后填充了 3 个字节的使得 b1.i 在 4 上对齐. 在 b1.s 后填充 2 个字节是为了让结构体变量本身的大小为其有效对齐值的整数倍.

1
2
3
4
5
6
struct C{
char c1; // offset = 0
char c2; // offset = 1
char c3; // offset = 2
};
struct C c;

上面的结构体 A 和结构体 B 的自身对齐值都是 4, 所以结构体变量的起始地址需要在 4 上对齐. 考虑一下结构体 C, 其有效对齐值为 1, 是不需要额外进行对齐的.