如何编写编译器可以有效编译到SSE或AVX的c++代码



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

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

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

自Nehalem以来,未对齐的加载/存储指令在英特尔处理器上具有相同的性能。然而,在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];
    }
}

4,然后CCD_ 5给出160行。而如果使用__builtin_assume_aligned,则wc test.s仅给出45行。当我在这两种情况下都用叮当声返回110行时。

因此,当clang通知编译器数组已经对齐时(在这种情况下)没有什么区别,但使用GCC确实有区别。计算代码行数并不是衡量性能的充分指标,但我不会在这里发布所有的程序集。我只是想说明,当编译器被告知数组对齐时,它可能会产生非常不同的代码。

当然,GCC由于没有假设阵列对齐而产生的额外开销在实践中可能没有什么不同。你必须测试并查看。


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


编辑:

GCC中的程序集包含许多不属于文本段的无关行。执行gcc -O3 -march=nehalem -S test.c,然后使用objdump -d,并计算文本(代码)段中的行数,得到不使用__builtin_assume_aligned108行,而只使用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年末)以来,这在处理器上应该很好。但是MSVC仍然有针对不是四的倍数的数组的清理代码,即使我告诉编译器它是四的倍数((n & (-4))。至少GCC做对了。


由于AVX可以折叠未延迟的负载,我用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];
    }
}

在没有CCD_ 17的情况下,GCC生产168条装配线,而有了CCD_。

我的原始答案变得太乱了,无法编辑,所以我在这里添加了一个新答案,并将我的原始回答社区设为wiki。

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

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

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

但自Nehalem以来,缓存线拆分的惩罚很小。事实证明,GCC为检查和对齐内存而添加的额外代码弥补了缓存行拆分带来的小损失。这就解释了为什么Clang和MSVC都不担心使用矢量化进行缓存线拆分。

因此,自Nehalem以来,我最初声称自动矢量化不需要知道对齐情况,这或多或少是正确的。这与说自尼哈莱姆以来,调整记忆毫无用处不是一回事。

最新更新