gpt4 book ai didi

c++ - 如何在结构中组织成员以在对齐时浪费最少的空间?

转载 作者:行者123 更新时间:2023-12-02 00:36:09 24 4
gpt4 key购买 nike

[不是 Structure padding and packing 的拷贝| .这个问题是关于填充发生的方式和时间。这是关于如何处理它。]
我刚刚意识到 C++ 中的对齐会浪费多少内存。考虑以下简单示例:

struct X
{
int a;
double b;
int c;
};

int main()
{
cout << "sizeof(int) = " << sizeof(int) << '\n';
cout << "sizeof(double) = " << sizeof(double) << '\n';
cout << "2 * sizeof(int) + sizeof(double) = " << 2 * sizeof(int) + sizeof(double) << '\n';
cout << "but sizeof(X) = " << sizeof(X) << '\n';
}
使用 g++ 时,程序给出以下输出:
sizeof(int) = 4
sizeof(double) = 8
2 * sizeof(int) + sizeof(double) = 16
but sizeof(X) = 24
那是 50% 的内存开销!在 134'217'728 X 的 3GB 数组中s 1 GB 将是纯填充。
幸运的是,问题的解决方法很简单——我们只需交换 double bint c大约:
struct X
{
int a;
int c;
double b;
};

现在结果更令人满意了:
sizeof(int) = 4
sizeof(double) = 8
2 * sizeof(int) + sizeof(double) = 16
but sizeof(X) = 16
但是有一个问题:这不是交叉兼容的。是的,在 g++ 下 int是 4 个字节和一个 double是 8 个字节,但这不一定总是正确的(它们的对齐也不必相同),因此在不同的环境下,此“修复”不仅无用,而且还可能通过增加需要的填充量。
有没有可靠的跨平台方式来解决这个问题 (最小化所需的填充量 而不会因未对齐而导致性能下降 )? 为什么编译器不执行这样的优化 (交换结构/类成员以减少填充)?
澄清
由于误解和混淆,我想强调的是 我不想“打包”我的 struct .也就是说,我不希望它的成员未对齐而因此访问速度较慢。相反,我仍然希望所有成员都是自对齐的,但在填充时使用最少的内存。这可以通过使用例如此处和 The Lost Art of Packing 中所述的手动重新排列来解决。通过埃里克雷蒙德。我正在寻找一种自动化的、尽可能多的跨平台方式来做到这一点,类似于 proposal P1112 中描述的内容对于即将到来的 C++20 标准。

最佳答案

(不要不假思索地应用这些规则。请参阅 ESR 关于您一起使用的成员的缓存位置的观点。并且在多线程程序中,要注意由不同线程编写的成员的错误共享。通常您不希望每个线程的数据在出于这个原因,根本就只有一个结构,除非你这样做是为了用大 alignas(128) 控制分隔。这适用于 atomic 和非原子变量;重要的是线程写入缓存行,而不管它们如何去做。)

经验法则:从大到小 alignof() .没有任何事情可以做到在任何地方都是完美的,但到目前为止,最常见的情况是针对普通 32 位或 64 位 CPU 的理智的“普通”C++ 实现。所有原始类型都具有 2 的幂大小。
大多数类型都有alignof(T) = sizeof(T) , 或 alignof(T)以实现的寄存器宽度为上限。所以较大的类型通常比较小的类型更对齐。
大多数 ABI 中的结构打包规则为结构成员提供了绝对值 alignof(T)相对于结构体的开头对齐,结构体本身继承最大的alignof()其任何成员。

  • 将始终 64 位成员放在首位 (如 doublelong longint64_t )。 ISO C++ 当然不会将这些类型修复为 64 位/8 字节,但实际上在您关心的所有 CPU 上都是如此。将您的代码移植到异国 CPU 的人员可以在必要时调整结构布局以进行优化。
  • 然后指针 和指针宽度整数:size_t , intptr_t , 和 ptrdiff_t (可能是 32 位或 64 位)。对于具有扁平内存模型的 CPU,这些在普通现代 C++ 实现中的宽度都是相同的。
    如果您关心 x86 和 Intel CPU,请考虑首先放置链表和树左/右指针。通过树或链表中的节点进行指针追逐 has penalties when the struct start address is in a different 4k page than the member you're accessing .将他们放在首位保证不可能是这种情况。
  • 然后 long (在 LLP64 ABI 中,例如 Windows x64,即使指针是 64 位,有时也是 32 位)。但它保证至少与 int 一样宽.
  • 然后是 32 位 int32_t , int , float , enum . (可选地将 int32_tfloat 放在 int 之前,如果您关心仍将这些类型填充为 32 位的可能的 8/16 位系统,或者在它们自然对齐的情况下做得更好。大多数此类系统不会具有更宽的负载(FPU 或 SIMD),因此无论如何必须始终将更宽的类型作为多个单独的块进行处理)。
    ISO C++ 允许 int窄至 16 位,或任意宽,但实际上即使在 64 位 CPU 上也是 32 位类型。 ABI 设计者发现设计用于 32 位的程序 int如果 int 只是浪费内存(和缓存占用)更宽。不要做出会导致正确性问题的假设,但是对于“可移植性能”,您只需要在正常情况下是正确的。
    如有必要,人们可以针对异国平台调整您的代码。 如果某个结构布局对性能至关重要,也许可以在标题中评论您的假设和推理。
  • 然后 short/int16_t
  • 然后 char/int8_t/bool
  • (对于多个 bool 标志,特别是如果主要是读取或者如果它们一起被修改,请考虑用 1 位位域打包它们。)

  • (对于无符号整数类型,请在我的列表中找到相应的有符号类型。)
    如果您愿意,可以更早地使用更窄类型的 8 个字节的倍数数组。但是如果你不知道类型的确切大小,你不能保证 int i + char buf[4]将填充两个 double 之间的 8 字节对齐槽s。但这不是一个糟糕的假设,所以如果有某种原因(例如成员的空间局部性一起访问)将它们放在一起而不是最后,我还是会这样做。
    异国情调 : 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) 的聚合(结构或 union )时,它会变得更加棘手。 .
    另一个转折点是,在某些 ABI 中(例如,32 位 Windows,如果我没记错的话)结构成员相对于结构的开头与它们的大小(最多 8 个字节)对齐,即使 alignof(T) double 仍然只有 4和 int64_t .
    这是为了针对单个结构单独分配 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. Readability and cache locality

    While reordering by size is the simplest way to eliminate slop, it’s not necessarily the right thing. There are two more issues: readability and cache locality.


    在一个可以很容易地跨缓存行边界拆分的大型结构中,如果它们总是一起使用,那么将它们放在附近是有意义的。甚至连续以允许加载/存储合并,例如使用一个(未对齐的)整数或 SIMD 加载/存储复制 8 或 16 个字节,而不是单独加载较小的成员。
    在现代 CPU 上,缓存线通常为 32 或 64 字节。 (在现代 x86 上,始终为 64 字节。Sandybridge 系列在 L2 缓存中有一个相邻行空间预取器,它尝试完成 128 字节的行对,与主要的 L2 流媒体硬件预取模式检测器和 L1d 预取分开)。

    有趣的事实:Rust 允许编译器为了更好的打包或其他原因对结构重新排序。不过,如果有任何编译器确实这样做的话,IDK。如果您希望选择基于结构的实际使用方式,则可能只有在链接时整个程序优化中才有可能。否则程序的单独编译部分无法就布局达成一致。

    (@alexis 发布了一个链接到 ESR 文章的仅链接答案,所以感谢这个起点。)

    关于c++ - 如何在结构中组织成员以在对齐时浪费最少的空间?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56761591/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com