字节对齐
字节对齐
计算机中内存大小的基本单位是字节(byte),理论上来讲,可以从任意地址访问某种基本数据类型。 但是实际上,计算机并非逐字节大小读写内存,而是以2,4,8…的倍数的字节块来读写内存,如此一来就会对基本数据类型的合法地址作出一些限制,即它的地址必须是2,4,8…的倍数。那么就要求各种数据类型按照一定的规则在空间上排列,这就是字节对齐。
字节对齐机制主要是为了访存效率,因为对齐的字节访存效率更高。
计算机底层存储硬件比如内存、CPU cache、寄存器等的访问都不是一次一个字节而是一次一批/一组:
- 字(word):CPU指令处理的数据单元,分为WORD(16bits)、DWORD(32bits)、QWORD(64bits)
- 寄存器(register):CPU通用寄存器通常是64bits,也允许访问寄存器的前8bits、前16bits、前32bits。
- 缓存行(cache line):长度通常是512bits,即64字节。
- 页(page):一个页大小通常是4096字节。 例如一个4字节的整除原本只需要一次访存,如果组织不当导致跨过了两个cache line的边界则需要两次访存,开销高了一倍!
所以字节对齐的本质就是在内存空间占用和访存效率之间做折中
。
struct字节对齐准则:
- 不允许对结构体成员进行重排序,即成员的内存排列顺序一定是定义顺序
- 结构体第一个成员的偏移量(offset)为0
- 结构体每个成员相对第一个成员的offset都是其size的整数倍,如不满足,对前一个成员填充字节以满足。
- 结构体的总size为结构体中最大成员的size的整数倍,如不满足,在结构体最后填充字节以满足。
- 结构体嵌套时,递归的按照结构体中最大的那个成员对齐。(注意不是最大的结构体,而是结构体中最大的那个成员)
#include <cstdio>
#include <cstdint>
// 空结构体和空类所占内存大小为1
// 编译器会给空类/结构体隐式的加一个字节,确保每个实例在内存中有唯一的地址
struct DemoStruct {
};
struct DemoStruct0 {
char c1; // 1B
int i1; // 4B
uint64_t n1; // 8B
};
struct DemoStruct1 {
char c1; // 1B
uint64_t n1; // 8B
int i1; // 4B
};
int main()
{
// 1
printf("sizeof(DemoStruct)=%lu\n", sizeof(DemoStruct));
// 16
printf("sizeof(DemoStruct0)=%lu\n", sizeof(DemoStruct0));
// 24
printf("sizeof(DemoStruct1)=%lu\n", sizeof(DemoStruct1));
return 0;
}
再看嵌套struct的例子:
#include <cstdio>
#include <cstdint>
#include <cstddef>
struct DemoStruct2 {
short s1; // 2B
short s2; // 2B
// Padding 4B(因为sdb1结构体是8字节对齐的, 所以sdb1.sc1相对于首地址的偏移必须是8的倍数,所以在sdb1.sc1之前填充4B)
struct SubDemoStruct1 {
char sc1; // 1B
// Padding 7B
long long int slli1; // 8B
} sdb1;
struct SubDemoStruct2 {
char sc1; // 1B
} sdb2;
// Padding 3B(因为sdb3结构体是4字节对齐的, 所以sdb3.si1相对于首地址的偏移必须是4的倍数,所以在sdb3.si1之前填充3B)
struct SubDemoStruct3 {
int si1; // 4B
} sdb3;
char c1; // 1B
// Padding 7B(因为结构体中最大成员的size是8, 所以结构体的总大小必须是8的倍数,所以在结构体最后填充7B)
};
int main()
{
DemoStruct2 ds2;
printf("sizeof(DemoStruct2)=%lu\n", sizeof(ds2));
printf("DemoStruct2 结构中的 s1 偏移 = %lu 字节。\n", offsetof(struct DemoStruct2, s1));
printf("DemoStruct2 结构中的 s2 偏移 = %lu 字节。\n", offsetof(struct DemoStruct2, s2));
printf("DemoStruct2 结构中的 sdb1.sc1 偏移 = %lu 字节。\n", offsetof(struct DemoStruct2, sdb1.sc1));
printf("DemoStruct2 结构中的 sdb1.slli1 偏移 = %lu 字节。\n", offsetof(struct DemoStruct2, sdb1.slli1));
printf("DemoStruct2 结构中的 sdb2.sc1 偏移 = %lu 字节。\n", offsetof(struct DemoStruct2, sdb2.sc1));
printf("DemoStruct2 结构中的 sdb3.si1 偏移 = %lu 字节。\n", offsetof(struct DemoStruct2, sdb3.si1));
printf("DemoStruct2 结构中的 phone 偏移 = %lu 字节。\n", offsetof(struct DemoStruct2, c1));
return 0;
}
输出:
sizeof(DemoStruct2)=40
DemoStruct2 结构中的 s1 偏移 = 0 字节。
DemoStruct2 结构中的 s2 偏移 = 2 字节。
DemoStruct2 结构中的 sdb1.sc1 偏移 = 8 字节。
DemoStruct2 结构中的 sdb1.slli1 偏移 = 16 字节。
DemoStruct2 结构中的 sdb2.sc1 偏移 = 24 字节。
DemoStruct2 结构中的 sdb3.si1 偏移 = 28 字节。
DemoStruct2 结构中的 phone 偏移 = 32 字节。
C/C++编译器会自动处理struct的内对齐,同时提供了一些机制让程序员手动控制内存对齐(#pragma pack directive)。
#include <stdio.h>
#include <stdint.h>
struct DemoStruct {
char c1; // 1B
uint64_t n1; // 8B
};
#pragma pack(1) // 显式指定字节对齐规则
struct DemoPackStruct {
char c1; // 1B
uint64_t n1; // 8B
};
#pragma pack(2)
struct DemoPack2Struct {
char c1; // 1B
uint64_t n1; // 8B
};
#pragma pack() // 设置为默认字节对齐规则
struct DemoPack3Struct {
char c1; // 1B
uint64_t n1; // 8B
};
int main() {
// 16
printf("sizeof(DemoStruct)=%d\n", sizeof(DemoStruct));
// 9
printf("sizeof(DemoPackStruct)=%d\n", sizeof(DemoPackStruct));
// 10
printf("sizeof(DemoPack2Struct)=%d\n", sizeof(DemoPack2Struct));
// 16
printf("sizeof(DemoPack3Struct)=%d\n", sizeof(DemoPack3Struct));
return 0;
}
pragma pack用法:
#pragma pack(show) // 显示当前内存对齐的字节数,编辑器默认4字节对齐
#pragma pack(n) // 设置编译器按照n个字节对齐,n可以取值1,2,4,8,16
#pragma pack(0) // 恢复编译器默认对齐方式
#pragma pack() // 恢复编译器默认对齐方式
#pragma pack(push) // 将当前的对齐字节数压入栈顶,不改变对齐字节数
#pragma pack(push, n) // 将当前的对齐字节数压入栈顶,并按照n字节对齐
#pragma pack(pop) // 弹出栈顶对齐字节数,不改变对齐字节数
#pragma pack(pop, n) // 弹出栈顶并直接丢弃,按照n字节对齐
说明:
- #pragma pack(push): push是“压入”的意思。编译器编译到此处时将保存对齐状态(保存的是push指令之前的对齐状态)。
- #pragma pack(pop): pop是”弹出“的意思。编译器编译到此处时将恢复push指令前保存的对齐状态(请在使用该预处理命令之前使用#pragma pack(push))。
push和pop是一对应该同时出现的名词,只有pop没有push不起作用,只有push没有pop可以保持之前对齐状态(但是这样就没有使用push的必要了)。
#pragma pack()是取消自定义对齐方式,恢复默认方式,而push之后pop是回到push指令之前的对齐方式。