您可以看到使用 GNU C / C++ 对代码生成的影响__builtin_assume_aligned
。
针对 x86(和 ICC18)的 gcc 7 和更早版本更喜欢使用标量序言来到达对齐边界,然后是对齐的向量循环,然后是标量尾声来清理任何不是完整向量倍数的剩余元素。
考虑在编译时已知元素总数是向量宽度的倍数但不知道对齐方式的情况。 如果您知道对齐方式,则不需要序言或结语。但如果没有,你需要两者。 最后一个对齐向量之后的剩余元素的数量是未知的。
此Godbolt 编译器资源管理器链接显示了为使用 ICC18、gcc7.3 和 clang6.0 的 x86-64 编译的这些函数。clang非常积极地展开,但仍然使用未对齐的商店。这似乎是一种将这么多代码大小用于仅存储的循环的奇怪方式。
// aligned, and size a multiple of vector width
void set42_aligned(int *p) {
p = (int*)__builtin_assume_aligned(p, 64);
for (int i=0 ; i<1024 ; i++ ) {
*p++ = 0x42;
}
}
# gcc7.3 -O3 (arch=tune=generic for x86-64 System V: p in RDI)
lea rax, [rdi+4096] # end pointer
movdqa xmm0, XMMWORD PTR .LC0[rip] # set1_epi32(0x42)
.L2: # do {
add rdi, 16
movaps XMMWORD PTR [rdi-16], xmm0
cmp rax, rdi
jne .L2 # }while(p != endp);
rep ret
这几乎完全是我手动执行的操作,除了可能会展开 2,因此 OoO exec 可以发现循环出口分支未被采用,同时仍在咀嚼商店。
因此未对齐的版本包括序言和尾声:
// without any alignment guarantee
void set42(int *p) {
for (int i=0 ; i<1024 ; i++ ) {
*p++ = 0x42;
}
}
~26 instructions of setup, vs. 2 from the aligned version
.L8: # then a bloated loop with 4 uops instead of 3
add eax, 1
add rdx, 16
movaps XMMWORD PTR [rdx-16], xmm0
cmp ecx, eax
ja .L8 # end of main vector loop
# epilogue:
mov eax, esi # then destroy the counter we spent an extra uop on inside the loop. /facepalm
and eax, -4
mov edx, eax
sub r8d, eax
cmp esi, eax
lea rdx, [r9+rdx*4] # recalc a pointer to the last element, maybe to avoid a data dependency on the pointer from the loop.
je .L5
cmp r8d, 1
mov DWORD PTR [rdx], 66 # fully-unrolled final up-to-3 stores
je .L5
cmp r8d, 2
mov DWORD PTR [rdx+4], 66
je .L5
mov DWORD PTR [rdx+8], 66
.L5:
rep ret
即使对于一个更复杂的循环,它会从一点点展开中受益,gcc 让主矢量化循环根本不展开,而是在完全展开的标量序言/结尾处花费大量代码大小。uint16_t
对于带有元素或其他东西的 AVX2 256 位矢量化来说,这真的很糟糕。(序言/尾声中最多 15 个元素,而不是 3 个)。这不是一个明智的权衡,因此它有助于 gcc7 和更早的版本在指针对齐时告诉它。(执行速度变化不大,但对减少代码膨胀有很大影响。)
顺便说一句,gcc8 倾向于使用未对齐的加载/存储,假设数据通常是对齐的。现代硬件具有廉价的未对齐的 16 和 32 字节加载/存储,因此让硬件处理跨缓存线边界拆分的加载/存储的成本通常是好的。(AVX512 64 字节存储通常值得对齐,因为任何未对齐都意味着每次访问时都会拆分缓存行,而不是每隔一个或每 4 个。)
另一个因素是,与在开始/结束处执行一个未对齐的潜在重叠向量的智能处理相比,较早的 gcc 完全展开的标量序言/尾声是废话。(请参阅此手写版本的尾声set42
)。如果 gcc 知道如何做到这一点,那就值得更频繁地调整。