如何在GCC中获得更好的矢量化?

5
考虑以下三个执行相同计算的函数:

#include <x86intrin.h>

void testfunc_loop(double a, double b, double* dst)
{
    double f[] = {a,b,-a,-b};

    for(int n = 0; n < 4; ++n)
    {
        dst[n] = 0.1 + f[n]*(1.0 + 0.5*f[n]);
    }
}

void testfunc_flat(double a, double b, double* dst)
{
    dst[0] = 0.1 + ( a)*(1.0 + 0.5*( a));
    dst[1] = 0.1 + ( b)*(1.0 + 0.5*( b));
    dst[2] = 0.1 + (-a)*(1.0 + 0.5*(-a));
    dst[3] = 0.1 + (-b)*(1.0 + 0.5*(-b));
}

void testfunc_avx(double a, double b, double* dst)
{
    __m256d one      = _mm256_set1_pd(1.0);
    __m256d half     = _mm256_set1_pd(0.5);
    __m256d tenth    = _mm256_set1_pd(0.1);

    __m256d v = _mm256_set_pd(-b,-a,b,a);

    __m256d q = _mm256_add_pd(tenth,_mm256_mul_pd(v,_mm256_add_pd(one,_mm256_mul_pd(half,v))));

    _mm256_store_pd(dst,q);
}

GCC 4.7.2(使用-O3 -mavx)向量化循环版本,但对展开循环使用标量操作。三个版本所花费的时间(标准化后)分别为3.3(循环,自动矢量化)、1.2(展开,标量)、1(手动AVX)。展开版本和手动矢量化函数之间的性能差异很小,但我想强制进行矢量化,因为它在整个代码中是有益的。
使用不同编译器进行测试(参见https://godbolt.org/g/HJH2CX)显示,clang自动向量化展开循环(从版本3.4.1开始),但GCC直到版本7仍未实现。我是否可以在GCC中自动获得类似的向量化?我只找到与循环向量化相关的优化选项,这些选项并没有帮助。GCC网站自2011年以来没有更新。

2
请注意,在gcc的bugzilla中提交有关未实现优化的问题是明智的做法。AVX向量化失败是因为带/不带否定的表达式看起来与gcc太不同。另一方面,它几乎进行了SSE向量化,但拒绝了它,因为它被高估了前言成本(它至少构建了向量{a,b}三次,{1,1}两次等),可以使用-fvect-cost-model = unlimited查看它将产生什么结果。 - Marc Glisse
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78164 是关于 SSE 向量化 testfunc_flat 的问题,LLVM 可以实现,但 GCC 不能。 - Marc Glisse
1个回答

5

gcc通常不会将单向量内容进行矢量化。在现有代码库(Endless Sky)中,我看到了类似的自动矢量化缺失,这是由于Point{ double x,y; }类。

因此,如果您需要内联到快速代码中,则可能需要手动将其向量化为x86。(您还可以考虑传递__m256d值而不是存储到数组中。)

BTW, the manually-vectorized version could be faster. I played around with it on Godbolt, and noticed that _mm256_set_pd(-b,-a, b,a) was compiling to stupid code, so it would be more efficient to do it manually. Also, if you don't have FMA available, you can reduce the latency by re-factoring the expression. (allowing 0.1 -/+ a to happen in parallel with squaring). Code+asm here

// 0.1 + a  + 0.5*a*a   =  0.1 +   a  * (1.0 + 0.5*a)
//     + b
// 0.1 - a  + 0.5*a*a   =  0.1 + (-a) * (1.0 - 0.5*a)
//     - b

// only one of the mul+add pairs can fuse into an FMA
// but 0.1+/-a happens in parallel with 0.5*a*a, so it's lower latency without FMA
void testfunc_latency_without_fma(double a, double b, double* dst)
{
  // 6 AVX instructions other than the store:
  // 2 shuffles, 1 mul, 1 FMA, 1 add.  1 xor.  In theory could run one iteration per 2 clocks
    __m256d abab       = _mm256_setr_pd(a, b, a, b);    // 1c + 3c latency (unpck + vinsertf128)
    __m256d sq256      = _mm256_mul_pd(abab, abab);     // 5c
    const __m256d half = _mm256_set1_pd(0.5);
    __m256d sq_half256 = _mm256_mul_pd(sq256, half);    // 5c: dependency chain 1 ready in 14c from a and b being ready

    // we could use a smaller constant if we do _mm256_setr_m128d(ab, xor(ab, set1(-0.))
    // but that takes an extra vinsertf128 and this part isn't the critical path.
    const __m256d upper_signmask = _mm256_setr_pd(0. ,0. ,-0. ,-0.);
    __m256d ab_negab = _mm256_xor_pd(abab, upper_signmask); // chain2: 1c from abab

    const __m256d tenth   = _mm256_set1_pd(0.1);
    __m256d tenth_plusminus_ab = _mm256_add_pd(tenth, ab_negab); // chain2: 3c (ready way ahead of squared result)

    __m256d result = _mm256_add_pd(tenth_plusminus_ab, sq_half256);  // fuses with the sq_half
    _mm256_store_pd(dst, result);
}

我不知道为什么你测试时自动向量化循环如此缓慢。它将标量存储到数组中,然后进行矢量加载,产生约11个周期的存储转发停顿。因此,它比其他两种方式具有更高的延迟,但我不知道这是否会影响吞吐量。我不知道你是如何测试的;也许你是将一个调用的结果用作下一个调用的输入?或者重复在同一块堆栈空间上进行存储转发停顿是否是一个问题?


通常情况下,对于较大的数组,gcc希望指针对齐。它生成巨大的完全展开的标量引导/退出代码以达到对齐指针,然后使用对齐存储/加载。

对于现代CPU来说,这对于通常运行时对齐的数据并没有太大帮助(但通常也不会有太大伤害),但如果数据通常未对齐,或者正在运行早于 Nehalem 的CPU上,则可能很好。

我不知道这是否与gcc不愿自动向量化小型内容有关,但告诉它double*对齐似乎没有帮助。

我认为问题的一部分是它不擅长插入洗牌以将需要洗牌的代码向量化。


感谢您的详细回答。我正在使用循环测试函数,并重复调用相同的参数并对结果求和。看来我必须坚持手动向量化。 - stardt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接