结构体的内存对齐
C++ 中结构体有内存对齐的机制,其最直观的现象就是一个结构体的总大小往往不是其中各个成员的大小简单加和,实际上,一个结构体的实际大小总是更大的。
这个现象的根因就是因为结构体的内存对齐机制,这个机制的目的是满足结构体中的每一个成员各自的内存对齐要求,并且让结构体整体也满足其内存对齐要求。
这是一个比较基础的概念,但是在我最早接触的时候,有很多疑问,其中最大的一个疑问就是:什么叫对齐?
内存对齐
CPU 在访问数据的时候,为了更加高效的访问数据,对数据在内存中的位置有一定的要求。
例如,对于一个 4 字节的 int 类型的数据,CPU 会将其存储在内存中的 4 字节对齐的位置上,也就是说,这个数据的起始字节地址必须是 4 的倍数。如果这个数据的位置不满足条件,那么它会被调整到满足条件的位置上,编译器会承担这个工作,这个过程就叫做对齐。
对于结构体中的成员,编译器会在这些成员之间插入填充字节,以满足结构体的内存对齐要求,例如:
struct S {
char a; // offset 0,占 1 字节
// padding 3 字节
int b; // offset 4,占 4 字节
short c; // offset 8,占 2 字节
// padding 2 字节
};
上面这个结构体的各个成员大小加起来只有 7 字节,但是实际上因为内存对齐的原因,这个结构体真实的大小是 12 字节。
内存对齐的两个规则
还是按照上面的例子解释,编译器在 b 之前插入了空白字节,因为 b 必须要在 4 字节对齐的位置上,而 a 只占了 1 字节,所以需要插入 3 字节的空白字节。而 c 后面也插入了 2 字节的填充字节,则是因为结构体的整体大小也需要对齐,也就是结构体的总大小必须是其成员中最大对齐要求的倍数。b 是结构体中具有最大对齐要求的成员,那么结构体的整体大小也必须 4 字节对齐。
总结来说,C++ 结构体的内存对齐主要是为了让 CPU 更高效地访问数据。核心规则可以概括为:每个成员按自己的对齐要求放置,结构体整体大小按最大对齐要求补齐。
修改与查看对齐规则
内存对齐虽然有默认的规则,但并非不能改变。C++ 提供了手动设置对齐规则的关键字:alignas。
alignas 有两种用法,一种是改变整个结构体的对齐规则:
struct alignas(16) S {
int x;
};
这会让结构体 S 以 16 字节对齐,同时,结构体的整体大小也会以 16 字节对齐。
另一种用法是改变单个成员的对齐规则:
struct S {
alignas(8) int x;
};
这会让成员 x 以 8 字节对齐,而结构体的整体大小仍然以默认的对齐规则补齐。
还可以用 alignof 查看对齐规则:
alignof(S) // 输出 16
最佳实践
结构体虽简单,但是因为有内存对齐规则的存在,也有一些小技巧。 在一些性能敏感或者是内存敏感的场景里,最好把结构体里的数据成员手动排个序,把对齐大的成员放在前面,对齐小的放在后面,这样能够让一个结构体的大小尽可能地小,从而减少内存占用和提高性能。例如:
struct A {
char a;
int b;
char c;
};
通常:
sizeof(A) == 12
而调整顺序:
struct B {
int b;
char a;
char c;
};
通常:
sizeof(B) == 8