【发布时间】:2021-12-30 20:57:40
【问题描述】:
众所周知,GCC/CLang 使用 SIMD 指令可以很好地自动矢量化循环。
众所周知,存在alignas()标准C++属性,除其他用途外,它还允许对齐堆栈变量,例如以下代码:
#include <cstdint>
#include <iostream>
int main() {
alignas(1024) int x[3] = {1, 2, 3};
alignas(1024) int (&y)[3] = *(&x);
std::cout << uint64_t(&x) % 1024 << " "
<< uint64_t(&x) % 16384 << std::endl;
std::cout << uint64_t(&y) % 1024 << " "
<< uint64_t(&y) % 16384 << std::endl;
}
输出:
0 9216
0 9216
这意味着x 和y 在堆栈上都在 1024 字节上对齐,但不是 16384 字节。
现在让我们看看另一个代码:
#include <cstdint>
void f(uint64_t * x, uint64_t * y) {
for (int i = 0; i < 16; ++i)
x[i] ^= y[i];
}
如果在 GCC 上使用 -std=c++20 -O3 -mavx512f 属性编译,它会生成以下 asm 代码(提供部分代码):
vmovdqu64 zmm1, ZMMWORD PTR [rdi]
vpxorq zmm0, zmm1, ZMMWORD PTR [rsi]
vmovdqu64 ZMMWORD PTR [rdi], zmm0
vmovdqu64 zmm0, ZMMWORD PTR [rsi+64]
vpxorq zmm0, zmm0, ZMMWORD PTR [rdi+64]
vmovdqu64 ZMMWORD PTR [rdi+64], zmm0
AVX-512 unaligned load + xor + unaligned store 两次。所以我们可以理解,我们的 64 位数组异或操作被 GCC 自动向量化以使用 AVX-512 寄存器,并且循环也展开了。
我的问题是如何告诉 GCC 提供给函数指针 x 和 y 都对齐到 64 个字节,这样我就可以强制 GCC 代替上面代码中的 unaligned load (vmovdqu64)使用aligned load (vmovdqa64)。众所周知,对齐的加载/存储可以相当快。
我第一次尝试强制 GCC 进行对齐加载/存储是通过以下代码:
#include <cstdint>
void g(uint64_t (&x_)[16],
uint64_t const (&y_)[16]) {
alignas(64) uint64_t (&x)[16] = x_;
alignas(64) uint64_t const (&y)[16] = y_;
for (int i = 0; i < 16; ++i)
x[i] ^= y[i];
}
但此代码仍会产生未对齐的负载 (vmovdqu64),与上面的 asm 代码(之前的代码 sn-p)相同。因此,这个alignas(64) 提示对改进 GCC 汇编代码没有任何帮助。
我的问题是如何强制 GCC 进行对齐的自动矢量化,除了为 _mm512_load_epi64() 等所有操作手动编写 SIMD 内在函数?
如果可能,我需要所有 GCC/CLang/MSVC 的解决方案。
【问题讨论】:
-
对齐加载指令不需要使用对齐加载:如果地址对齐,则加载对齐。参见例如choice between aligned vs. unaligned x86 SIMD instructions
-
@harold 你的意思是如果汇编代码包含未对齐的
vmovdqu64指令,并且如果我的指针是对齐的,那么该指令将在 CPU 内部解码为对齐指令,并且与对齐的速度相同?这是否意味着手动使用对齐的vmovdqa64根本不会加速任何事情,而不是一点点?如果没有一点加速,为什么还要在 CPU 中引入对齐指令? -
vmovdqa64在防止意外错位方面发挥了适度的作用。在过去(Core2 时代和更早的时代)movdqu使用对齐地址的效率明显低于movdqa,因此在当时它们是单独的指令更有意义。 -
@Arty 似乎引入它是为了让旧处理器更快,但它不再有用了。保留这些说明是为了向后兼容。所以是的,只要您不针对旧架构并且启用 AVX 以便使用 VEX 前缀(在 GCC/Clang/VS 中默认不启用 AVX),就不应该加快速度。只有当您的代码受指令解码的限制时,VEX 前缀的好处才会出现,这对于较新处理器上的良好 SIMD 代码来说不是很频繁(除非循环通过大量加载/存储积极展开)。
标签: c++ performance simd avx512