内存对齐
参考
为什么要进行内存对齐
尽管内存是以字节为单位进行划分的, 但是大部分处理器并不是按字节块来存取内存的. 它一般会以 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 | struct A{ |
对于结构体 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 | struct B{ |
对于结构体 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 | struct C{ |
上面的结构体 A 和结构体 B 的自身对齐值都是 4, 所以结构体变量的起始地址需要在 4 上对齐. 考虑一下结构体 C, 其有效对齐值为 1, 是不需要额外进行对齐的.