54 c++ optimization memory-alignment memory-layout struct-member-alignment
[不是结构填充和包装的重复。这个问题是关于填充的方式和时间。这是关于如何处理它的信息。]
我刚刚意识到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';
}
Run Code Online (Sandbox Code Playgroud)
使用g ++时,程序将提供以下输出:
sizeof(int) = 4
sizeof(double) = 8
2 * sizeof(int) + sizeof(double) = 16
but sizeof(X) = 24
Run Code Online (Sandbox Code Playgroud)
那是50%的内存开销!在134'217'728 Xs 的3 GB阵列中,1 GB将是纯填充。
幸运的是,解决问题的方法很简单-我们只需要交换double b和int c周围:
struct X
{
int a;
int c;
double b;
};
Run Code Online (Sandbox Code Playgroud)
现在的结果更加令人满意:
sizeof(int) = 4
sizeof(double) = 8
2 * sizeof(int) + sizeof(double) = 16
but sizeof(X) = 16
Run Code Online (Sandbox Code Playgroud)
但是,存在一个问题:这不是交叉兼容的。是的,在g ++下int,a是4个字节,a double是8个字节,但这不一定总是正确的(它们的对齐方式也不必相同),因此在不同的环境下,此“修复”不仅无效,而且通过增加所需的填充量,它还可能使情况变得更糟。
是否有可靠的跨平台方法来解决此问题(在不因未对齐而导致性能降低的情况下,最大限度地减少所需的填充量)?编译器为什么不执行这样的优化(交换结构/类成员以减少填充)?
由于误解和困惑,我想强调一点,我不想“打包”我的struct。也就是说,我不希望其成员不结盟,因此访问速度较慢。取而代之的是,我仍然希望所有成员都是自我对齐的,但是要在填充时使用最少的内存。这可以通过使用如此处所述和埃里克·雷蒙德(Eric Raymond)的《包装的迷失的艺术》中所述的手动重排来解决。我正在寻找一种自动化的跨平台方法来实现此目的,类似于即将发布的C ++ 20标准的提案P1112中所描述的方法。
Pet*_*des 34
(不要不加思索地应用这些规则。请参阅ESR关于您一起使用的成员的缓存局部性的观点。在多线程程序中,请提防由不同线程编写的成员的错误共享。通常,您不希望在每个线程中使用数据因此完全没有一个单一的结构,除非您使用它来控制大的分离alignas(128)。这适用于atomic非原子var;重要的是线程写入缓存行,而不管它们如何执行。
经验法则:从最大到最小alignof()。在任何地方都无法做到完美,但是到目前为止,最常见的情况是对于正常的32位或64位CPU而言,是一种理智的“正常” C ++实现。所有原始类型都具有2的幂数大小。
大多数类型都将alignof(T) = sizeof(T)或alignof(T)限制在实现的寄存器宽度处。因此,较大的类型通常比较小的类型更对齐。
大多数ABI中的结构打包规则赋予结构成员alignof(T)相对于结构开始的绝对对齐方式,并且结构本身继承了alignof()其成员中最大的一个。
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(在Windows x64之类的LLP64 ABI中,有时甚至是指针为64位时也为32位)。但保证至少与一样宽int。
然后32位int32_t,int,float,enum。(如果您担心可能的8/16位系统仍将这些类型填充为32位,或者将它们自然对齐会更好,则可以单独选择int32_t并float提前选择int。大多数此类系统没有较宽的负载(FPU或SIMD),因此无论如何,更广泛的类型必须始终作为多个单独的块进行处理)。
ISO C ++允许int窄至16位,或任意宽,但实际上,即使在64位CPU上,它也是32位类型。ABI设计人员发现,旨在与32位一起使用的程序int如果int宽度更大,只会浪费内存(和缓存占用空间)。不要做会导致正确性问题的假设,但是对于“便携式性能”,在通常情况下,您必须是正确的。
可以在需要时针对特定平台调整代码的人员可以进行调整。 如果某个特定的结构布局对性能至关重要,则可以在标头中评论您的假设和推理。
short/int16_tchar/ int8_t/boolbool标志,尤其是如果是只读标志,或者如果它们都被一起修改,请考虑将其与1位位域打包在一起。)(对于无符号整数类型,请在我的列表中找到相应的带符号类型。)
如果需要,可以使用较窄类型的8字节整数倍数组。但是,如果您不知道类型的确切大小,则不能保证int i+ char buf[4]将填充两个doubles 之间的8字节对齐插槽。但这不是一个糟糕的假设,因此,如果出于某种原因(例如,一起访问的成员的空间局部性)将他们放在一起而不是放在最后,我还是会这么做。
异国情调类型:X86-64系统V有alignof(long double) = 16,但i386的系统V只alignof(long double) = 4,sizeof(long double) = 12。它是x87 80位类型,实际上是10个字节,但填充为12或16,因此是其alignof的倍数,从而使数组成为可能而不会违反对齐保证。
通常,当您的结构成员本身是带有的集合(结构或联合)时,它将变得更加棘手sizeof(x) != alignof(x)。
另一个变化是,在某些ABI(例如,如果我没记错的话,是32位Windows)中,结构成员相对于结构的开头对齐其大小(最多8个字节),即使alignof(T)for 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)撰写了《结构包装的迷失的艺术》一文。具体而言,有关结构重排序的部分基本上是此问题的答案。
他还提出了另一个重要观点:
尽管按大小重新排序是消除倾斜的最简单方法,但这不一定是正确的选择。还有两个问题:可读性和缓存局部性。
在可以轻松地跨高速缓存行边界拆分的大型结构中,如果总是将两件东西一起使用,则将它们放置在附近是有意义的。甚至是连续的以允许加载/存储合并,例如使用一个(不需要的)整数或SIMD加载/存储复制8或16个字节,而不是分别加载较小的成员。
在现代CPU上,高速缓存行通常为32或64字节。(在现代x86上,始终为64个字节。Sandybridge系列在L2高速缓存中具有一个相邻行空间预取器,该预取器试图完成128字节的行对,与主要的L2流媒体硬件预取模式检测器和L1d预取分开)。
有趣的事实:Rust允许编译器出于更好的打包或其他原因对结构进行重新排序。但是,如果有任何编译器实际这样做,则使用IDK。如果您希望基于结构的实际使用方式进行选择,则只有在链接时整个程序优化中才有可能。否则,程序的单独编译部分将无法在布局上达成共识。
(@alexis发布了仅链接的答案,链接到ESR的文章,因此感谢您作为起点。)
Art*_*yer 31
gcc具有-Wpadded警告,当向结构中添加填充时会发出警告:
<source>:4:12: warning: padding struct to align 'X::b' [-Wpadded]
4 | double b;
| ^
<source>:1:8: warning: padding struct size to alignment boundary [-Wpadded]
1 | struct X
| ^
Run Code Online (Sandbox Code Playgroud)
And you can manually rearrange members so that there is less / no padding. But this is not a cross platform solution, as different types can have different sizes / alignments on different system (Most notably pointers being 4 or 8 bytes on different architectures). The general rule of thumb is go from largest to smallest alignment when declaring members, and if you're still worried, compile your code with -Wpadded once (But I wouldn't keep it on generally, because padding is necessary sometimes).
As for the reason why the compiler can't do it automatically is because of the standard ([class.mem]/19). It guarantees that, because this is a simple struct with only public members, &x.a < &x.c (for some X x;), so they can't be rearranged.
Nat*_*ica 14
There really isn't a portable solution in the generic case. Baring minimal requirements the standard imposes, types can be any size the implementation wants to make them.
To go along with that, the compiler is not allowed to reorder class member to make it more efficient. The standard mandates that the objects must be laid out in their declared order (by access modifier), so that's out as well.
You can use fixed width types like
struct foo
{
int64_t a;
int16_t b;
int8_t c;
int8_t d;
};
Run Code Online (Sandbox Code Playgroud)
and this will be the same on all platforms, provided they supply those types, but it only works with integer types. There are no fixed-width floating point types and many standard objects/containers can be different sizes on different platforms.
伙计,如果您有 3GB 数据,您可能应该通过其他方式解决问题,然后交换数据成员。
可以使用“数组结构”,而不是使用“结构数组”。所以说
struct X
{
int a;
double b;
int c;
};
constexpr size_t ArraySize = 1'000'000;
X my_data[ArraySize];
Run Code Online (Sandbox Code Playgroud)
将成为
constexpr size_t ArraySize = 1'000'000;
struct X
{
int a[ArraySize];
double b[ArraySize];
int c[ArraySize];
};
X my_data;
Run Code Online (Sandbox Code Playgroud)
每个元素仍然很容易访问mydata.a[i] = 5; mydata.b[i] = 1.5f;...。
没有填充(除了数组之间的几个字节)。内存布局是缓存友好的。预取器处理从几个单独的内存区域读取顺序内存块。
这并不像乍一看那么不正统。该方法广泛用于 SIMD 和 GPU 编程。
| 归档时间: |
|
| 查看次数: |
3273 次 |
| 最近记录: |