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