【问题标题】:How to write c++ code that the compiler can efficiently compile to SSE or AVX?如何编写编译器可以高效编译为 SSE 或 AVX 的 c++ 代码?
【发布时间】:2016-02-03 21:27:03
【问题描述】:

假设我有一个用 c++ 编写的函数,可以对很多向量执行矩阵向量乘法。它需要一个指向要转换的向量数组的指针。我是否正确假设编译器无法有效地将其优化为 SIMD 指令,因为它在编译时不知道传递的指针的对齐方式(SSE 需要 16 字节对齐或 AVX 需要 32 字节对齐)?还是数据的内存对齐方式与优化 SIMD 代码无关,数据对齐方式只会影响缓存性能?

如果对齐对生成的代码很重要,我怎样才能让(visual c++)编译器知道我打算只将具有某种对齐的值传递给函数?

【问题讨论】:

  • 当您说 AVX 时,您的意思是 256b 向量,对吗?因为编译器已经可以将通常的 _mm_whatever 内部函数编译为 SSE 或 128b-AVX(VEX 编码的 3 操作数)版本的指令。最好使用#defines 或包装函数来选择_mm_*_mm256_* 版本,特别是不方便。如果 256b 版本需要额外的置换,或者利用仅 AVX 指令。所以正如 Z Boson 所说,如果你能让编译器做好,那么自动向量化是你最好的选择。

标签: visual-c++ sse simd avx auto-vectorization


【解决方案1】:

从理论上讲,自从 Nehalem 以来,英特尔处理器上的对齐应该无关紧要。因此,您的编译器应该能够生成指针是否对齐不是问题的代码。

自 Nehalem 以来,未对齐的加载/存储指令在 Intel 处理器上的性能相同。然而,在 AVX 与 Sandy Bridge 一起到达之前,未对齐的负载无法通过另一个操作折叠以进行微操作融合。

此外,即使在 AVX 之前避免使用 16 字节对齐内存的高速缓存行拆分的惩罚仍然会有所帮助,因此编译器在指针对齐 16 字节之前添加代码仍然是合理的。

由于 AVX,使用对齐的加载/存储指令不再具有优势并且编译器没有理由添加代码以使指针 16 字节或 32 字节对齐。

但是,直到有理由使用对齐内存来避免 AVX 的缓存行拆分。因此,编译器添加代码以使指针 32 字节对齐是合理的,即使它仍然使用未对齐的加载指令。

所以在实践中,当一些编译器被告知假设指针是对齐的时,它们会生成更简单的代码。

我不知道有一种方法可以告诉 MSVC 指针已对齐。使用 GCC 和 Clang(自 3.6 起),您可以使用内置的 __builtin_assume_aligned。使用 ICC 和 GCC,您可以使用 #pragma omp simd aligned。使用 ICC,您还可以使用 __assume_aligned

例如用 GCC 编译这个简单的循环

void foo(float * __restrict a, float * __restrict b, int n)
{
    //a = (float*)__builtin_assume_aligned (a, 16);
    //b = (float*)__builtin_assume_aligned (b, 16);
    for(int i=0; i<(n & (-4)); i++) {
        b[i] = 3.14159f*a[i];
    }
}

gcc -O3 -march=nehalem -S test.c 然后wc test.s 给出 160 行。而如果使用 __builtin_assume_aligned 那么 wc test.s 只给出 45 行。当我在这两种情况下都使用 clang 返回 110 行时。

因此,当叮当声通知编译器数组对齐时没有任何区别(在这种情况下),但使用 GCC 确实如此。计算代码行数并不足以衡量性能,但我不会在此处发布所有程序集,我只是想说明您的编译器在被告知数组对齐时可能会生成非常不同的代码。

当然,GCC 因不假设数组是对齐的而产生的额外开销在实践中可能没有什么不同。您必须测试并查看。


无论如何,如果您想从 SIMD 中获得最大收益,我不会依赖编译器来正确完成它(尤其是使用 MSVC)。您的matrix*vector 示例总体上很差(但可能不适用于某些特殊情况),因为它受内存带宽限制。但是如果你选择matrix*matrix,没有很多不符合 C++ 标准的帮助,任何编译器都无法很好地优化它。在这些情况下,您将需要内在函数/内置函数/程序集,无论如何您都可以显式控制对齐方式。


编辑:

来自 GCC 的程序集包含许多不属于文本段的无关行。执行gcc -O3 -march=nehalem -S test.c 然后使用objdump -d 并计算文本(代码)段中的行数给出108 行而不使用__builtin_assume_aligned 并且只有16 行。这更清楚地表明,当 GCC 假定数组是对齐的时,它会产生非常不同的代码。


编辑:

我继续在 MSVC 2013 中测试了上面的 foo 函数。它产生未对齐的负载,并且代码比 GCC 短得多(我这里只显示主循环):

$LL3@foo:
    movsxd  rax, r9d
    vmulps  xmm1, xmm0, XMMWORD PTR [r10+rax*4]
    vmovups XMMWORD PTR [r11+rax*4], xmm1
    lea eax, DWORD PTR [r9+4]
    add r9d, 8
    movsxd  rcx, eax
    vmulps  xmm1, xmm0, XMMWORD PTR [r10+rcx*4]
    vmovups XMMWORD PTR [r11+rcx*4], xmm1
    cmp r9d, edx
    jl  SHORT $LL3@foo

这在 Nehalem(2008 年末)以来的处理器上应该没问题。但是即使我告诉编译器它是 4 的倍数 ((n &amp; (-4)),MSVC 仍然有针对不是 4 倍数的数组的清理代码。至少 GCC 做对了。


由于 AVX 可以折叠 unalinged 负载,因此我使用 AVX 检查了 GCC 以查看代码是否相同。

void foo(float * __restrict a, float * __restrict b, int n)
{
    //a = (float*)__builtin_assume_aligned (a, 32);
    //b = (float*)__builtin_assume_aligned (b, 32);
    for(int i=0; i<(n & (-8)); i++) {
        b[i] = 3.14159f*a[i];
    }
}

没有__builtin_assume_aligned GCC 会产生 168 行汇编,使用它只会产生 17 行。

【讨论】:

  • 即使在最新的 CPU 上,跨越缓存线边界 (64B) 的加载和存储仍然较慢。 Nehalem-and-later no-unaligned-penalty 仅适用于高速缓存行。对于顺序 32B 加载和存储,每隔一个将跨越两个缓存行。与缓存未命中相比,惩罚相当小,但除非您需要 很多 小分配,否则很可能。最好总是使用 32B 对齐的分配。或者使用预处理器检测目标是否支持 AVX,如果支持,则使用 32B 对齐分配。
  • 此外,对于 SSE,知道数据对齐允许​​编译器使用折叠加载到其他 SSE 指令中,而不是单独的 movups。 (非 mov 指令的 SSE 内存操作数的工作方式类似于 movaps。非 mov 指令的 AVX 内存操作数的工作方式类似于 movups。)
  • @PeterCordes,我并不是说对齐内存不再重要。我是说自从 Nehalem 之后,对齐的指令就不再重要了。编译器不需要知道指针是否对齐以生成最佳的自动矢量化代码。
  • @PeterCordes,关于您的第二条评论。我以为我们已经讨论过了。你甚至纠正了我的答案。折叠不需要我对齐。也许在硬件级别,但不是软件级别。编译器仍然可以将 movups 和 mulps 合并到一条指令中。看here。即使负载未对齐,MSVC 仍会折叠。 GCC 现在也这样做了。
  • 您所说的一切仅适用于 AVX。如果您想从同一代码中获得 非 AVX 目标的最佳代码,则需要告诉编译器数据已对齐。您链接的示例将未对齐的加载折叠到 VEX 编码的指令中,这是安全的。对非 VEX mulps 做同样的事情是安全的(即可能)。我认为一些编译器会在开始向量循环之前尝试到达一个对齐的指针,而不是仅仅使用输入对齐是什么,所以告诉编译器数据总是对齐可以消除庞大的死代码和一些启动检查。
【解决方案2】:

我的原始答案变得太乱而无法编辑,因此我在这里添加了一个新答案并制作了我的原始答案社区 wiki。

我在预 Nehalem 系统和带有 GCC、Clang 和 MSVC 的 Haswell 系统上使用对齐和未对齐内存进行了一些测试。

程序集显示只有 GCC 添加代码来检查和修复对齐。因此,__builtin_assume_aligned GCC 会产生更简单的代码。但是使用带有 Clang 的 __builtin_assume_aligned 只会将未对齐的指令更改为对齐(指令的数量保持不变)。 MSVC 只使用未对齐的指令。

性能结果是,在 per-Nehalem 系统上,当内存未对齐时,Clang 和 MSVC 比具有自动矢量化的 GCC 慢得多。

但是自从 Nehalem 以来,缓存行拆分的损失很小。事实证明,GCC 添加的用于检查和对齐内存的额外代码足以弥补由于缓存行拆分造成的小损失。这就解释了为什么 Clang 和 MSVC 都不担心向量化导致的缓存行拆分。

因此,自 Nehalem 以来,我最初声称自动矢量化不需要了解对齐方式的说法或多或少是正确的。这与说自 Nehalem 以来对齐内存没有用处不同。

【讨论】:

    猜你喜欢
    • 2014-01-16
    • 2020-01-31
    • 2011-04-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多