不要盲目应用这些规则。请参考ESR关于一起使用的成员的缓存局部性的观点。在多线程程序中,要注意不同线程写入的成员之间的虚假共享。通常情况下,出于这个原因,你不希望在单个结构体中有每个线程的数据,除非你正在使用
alignas(128)
来控制分离。这适用于原子和非原子变量;重要的是线程写入缓存行,而不管它们如何执行。
经验法则:从大到小排列
alignof()
。现在最常见的情况是针对普通32位或64位CPU的合理“正常”C++实现。所有基本类型都有2的幂次大小。
大多数类型的
alignof(T) = sizeof(T)
,或者
alignof(T)
被限制在实现的寄存器宽度上。因此,较大的类型通常比较小的类型具有更高的对齐方式。
大多数ABIs中的结构体打包规则使结构体成员相对于结构体开头具有其绝对
alignof(T)
对齐方式,并且结构体本身继承其任何成员中最大的
alignof()
。
首先放置始终为64位的成员(如double
、long long
和int64_t
)。当然,ISO C++并没有将这些类型固定在64位/8字节,但实际上,在您关心的所有CPU上,它们都是64位的。将代码移植到异域CPU的人可以调整结构布局以进行优化。
然后是指针和指针宽度的整数:size_t
、intptr_t
和ptrdiff_t
(可能是32位或64位)。在具有平面内存模型的正常现代C++实现中,它们的宽度都相同。
如果你关心x86和Intel CPU,考虑首先放置链表和树的左/右指针。当结构体起始地址与你要访问的成员不在同一个4k页面时,通过节点进行指针跟踪会有惩罚。将它们放在前面可以保证这种情况不会发生。
然后是long
(即使指针是64位的,在像Windows x64这样的LLP64 ABI中有时也是32位的)。但至少保证与int
一样宽。
然后是32位的int32_t
、int
、float
、enum
。(如果你关心可能将这些类型填充到32位的8/16位系统,或者自然对齐效果更好的系统,则可以选择在int
之前单独分离int32_t
和float
。大多数这样的系统没有更宽的负载(FPU或SIMD),因此更宽的类型必须始终作为多个单独的块处理。)
ISO C++允许int
最窄为16位,或任意宽度,但实际上即使在64位CPU上也是32位类型。ABI设计人员发现,如果int
更宽,那么为了适应32位的int
程序会浪费内存(和缓存占用)。不要做会导致正确性问题的假设,但对于“可移植性能”,您只需要在正常情况下做正确的事情。
为异域平台调整代码的人可以进行调整。如果某种结构布局对性能至关重要,也许可以在头文件中注释您的假设和推理。
然后是short
/int16_t
然后是char
/int8_t
/bool
(对于多个bool
标志,特别是如果它们大多数情况下只读或者它们都在一起修改,请考虑使用1位位域来打包它们。)
(对于无符号整数类型,请在我的列表中找到相应的有符号类型。)
如果你想要一个更窄的类型的8字节倍数的数组出现在前面,那么可以这样做。但是,如果你不知道类型的确切大小,你不能保证
int i
+
char buf[4]
会填充两个
double
之间的8字节对齐插槽。但这并不是一个坏的假设,所以如果有一些原因(比如一起访问的成员的空间局部性)将它们放在一起而不是放在最后,我仍然会这样做。
异类类型:x86-64 System V 的
alignof(long double) = 16
,但 i386 System V 只有
alignof(long double) = 4
,
sizeof(long double) = 12
。这是 x87 80位类型,实际上是10字节,但填充到12或16字节,使其成为其 alignof 的倍数,从而使数组可以在不违反对齐保证的情况下使用。
总的来说,当结构体成员本身是聚合体(结构体或联合体),且
sizeof(x) != alignof(x)
时,情况会变得更加棘手。
在某些ABIs中(例如32位Windows,如果我记得正确的话),结构成员会相对于结构的起始位置按其大小(最多8字节)对齐,即使对于double和int64_t,alignof(T)仍然只有4。这是为了优化单个结构的8字节对齐内存的常见情况,而不提供对齐保证。i386 System V对于大多数原始类型也具有相同的alignof(T)=4(但malloc仍然会给您8字节对齐的内存,因为alignof(maxalign_t)=8)。但是,无论如何,i386 System V都没有那个结构打包规则,因此(如果您没有按照从大到小的顺序排列结构),您可能会发现8字节成员与结构的起始位置不对齐。
大多数CPU都有寻址模式,可以通过寄存器中的指针访问任何字节偏移量。最大偏移通常非常大,但是在x86上,如果字节偏移适合于有符号字节([-128..+127]),则可以节省代码大小。因此,如果您有任何类型的大型数组,请优先将其放置在结构体中较后面的位置,而不是经常使用的成员之前。即使这样会稍微增加填充。
您的编译器几乎总会生成具有结构体地址的代码,而不是一些地址位于结构体中间以利用短负位移。
Eric S. Raymond写过一篇文章
The Lost Art of Structure Packing,其中
Structure reordering部分基本上回答了这个问题。
他还提出了另一个重要观点:
9. 可读性和高速缓存局部性
尽管按大小重新排序是消除浪费的最简单方法,但不一定是正确的方法。还有两个问题:可读性和高速缓存局部性。
在可以轻松跨越高速缓存线边界的
大型结构中,如果它们总是一起使用,则将2个元素放在附近是有意义的。甚至可以连续地进行加载/存储合并,例如使用一个(不对齐的)整数或SIMD加载/存储来复制8或16字节,而不是分别加载更小的成员。
现代CPU上的高速缓存行通常为32或64字节。(在现代x86上,始终为64字节。Sandybridge系列在L2缓存中具有相邻行空间预取器,试图完成128字节的行对,与主L2流处理程序HW预取模式检测器和L1d预取不同)。
有趣的事实:Rust允许编译器重新排列结构体以获得更好的打包效果或其他原因。虽然我不知道是否有任何编译器实际上这样做了。如果您希望选择基于结构体的实际使用情况,则可能仅在链接时进行整个程序优化才能实现。否则,程序的单独编译部分无法就布局达成一致。
(@alexis发布了一个只包含链接的答案,链接到ESR的文章,所以感谢这个起点。)
({{@alexis}}发布了一篇只有链接的回答,链接指向ESR的文章,因此感谢这个起点。)
struct foo { int a, b; };
这样的简单例子)。 - David C. Rankin