6

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

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

4

2 回答 2

5

理论上,自 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 假设数组是对齐的时,它会产生非常不同的代码。


编辑:

我继续foo在 MSVC 2013 中测试了上面的函数。它产生未对齐的负载,并且代码比 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 & (-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_alignedGCC 会产生 168 行组装,而使用 GCC 只会产生 17 行。

于 2015-11-04T10:00:06.030 回答
3

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

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

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

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

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

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

于 2015-11-16T10:49:37.223 回答