如何编写编译器可以高效编译为SSE或AVX的c ++代码?

时间:2015-11-03 16:17:43

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

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

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

2 个答案:

答案 0 :(得分:5)

理论上,自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];
    }
}

gcc -O3 -march=nehalem -S test.c然后wc test.s得到160行。然而,如果使用__builtin_assume_aligned,则wc test.s仅提供45行。当我在两种情况下都这样做时,clang返回110行。

因此,在clang通知编译器时,阵列对齐没有区别(在这种情况下),但与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年末)以来,处理器应该没问题。但MSVC仍然拥有不是4的倍数的数组的清理代码,即使我告诉编译器它是4的倍数((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];
    }
}

没有__builtin_assume_aligned GCC产生168行汇编,并且只有它产生17行。

答案 1 :(得分:2)

我的原始答案变得太乱了,无法编辑,所以我在这里添加一个新的答案并制作我的原始答案社区维基。

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

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

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

但是,自Nehalem以来,对缓存线分割的惩罚很小。事实证明,GCC增加的额外代码用于检查和对齐内存,而不是弥补由于缓存线分割造成的小额惩罚。这就解释了为什么Clang和MSVC都不担心带有矢量化的缓存行分裂。

因此,自从Nehalem以来,我自己最初声称自动纹理不需要知道对齐就或多或少是正确的。这与说Nehalem后对齐记忆无用是不一样的。