如何使用GCC实现更好的矢量化?

如何使用GCC实现更好的矢量化?,gcc,clang,sse,avx,auto-vectorization,Gcc,Clang,Sse,Avx,Auto Vectorization,考虑执行相同计算的这三个函数: #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(doubl

考虑执行相同计算的这三个函数:

#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);
}
#包括
void testfunc_循环(双a、双b、双*dst)
{
双f[]={a,b,-a,-b};
对于(int n=0;n<4;++n)
{
dst[n]=0.1+f[n]*(1.0+0.5*f[n]);
}
}
void testfunc_平坦(双a、双b、双*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(双a、双b、双*dst)
{
__m256d一个=mm256设置1个pd(1.0);
__m256d一半=_mm256_set1_pd(0.5);
__m256d第十位=_mm256_set1_pd(0.1);
__m256d v=_mm256_set_pd(-b,-a,b,a);
__m256d q=_mm256_add_pd(第十个,第五个,第二个,第五个,第五个);
_mm256_存储_pd(dst,q);
}
GCC4.7.2(使用-O3-mavx)对循环版本进行矢量化,但对展开的循环使用标量操作。三个版本的(标准化)时间分别为3.3(循环,自动矢量化),1.2(展开,标量),1(手动avx)。展开版本和手动矢量化函数之间的性能差异很小,但我想强制进行矢量化,因为它在完整代码中是有益的


使用不同的编译器进行测试(请参阅)表明,clang会自动对展开的循环进行矢量化(从3.4.1版开始),但直到7版的GCC不会。我可以用GCC自动获得类似的矢量化吗?我只找到了与循环矢量化相关的优化选项,这些选项没有帮助。自2011年以来没有新闻出现。

gcc通常不会对单个向量进行向量化。我在现有的代码库()中看到了类似的缺乏自动矢量化的
点{double x,y;}

因此,如果需要将x86内联到快速代码中,可能必须手动对其进行矢量化。(您也可以考虑绕过SyM256D< <代码>值,而不是存储到数组。)

顺便说一句,手动矢量化版本可能更快。我在Godbolt上玩过它,注意到
\u mm256\u set\u pd(-b,-a,b,a)
正在编译为愚蠢的代码,因此手动执行会更有效。此外,如果没有可用的FMA,可以通过重新分解表达式来减少延迟。(允许0.1-/+a与平方平行发生)


IDK为什么测试时自动矢量化循环如此缓慢。它在数组中进行标量存储,然后进行向量加载,导致~11个周期的存储转发暂停。因此,它比其他两种方法都有更高的延迟,但IDK可能会影响吞吐量。IDK你是如何测试的;也许你是在用一次通话的结果作为下一次通话的输入?或者,在相同的堆栈空间块上重复出现存储转发暂停是一个问题


一般来说,对于较大的数组,gcc非常喜欢对齐指针。它生成巨大的完全展开的标量intro/outro代码以到达对齐的指针,然后使用对齐的存储/加载

这对现代CPU没有多大帮助(但通常也不会造成太大伤害),尤其是对于通常在运行时对齐的数据,但如果数据通常未对齐,或者在Nehalem之前的CPU上运行,则这可能是好的

IDK如果这与gcc不愿意自动矢量化小事情有关,但告诉它,
double*
已对齐似乎没有帮助


我认为问题的一部分在于,它不擅长插入洗牌来对需要洗牌的代码进行矢量化。

感谢您提供了全面的答案。我在一个循环中测试函数,使用相同的参数重复调用,并对结果求和。看来我必须坚持手动矢量化。请注意,在gcc的bugzilla中为遗漏的优化提交问题是一件明智的事情。AVX矢量化失败,因为带/不带否定的表达式与gcc看起来太不一样。另一方面,它几乎实现了SSE向量化,但拒绝了它,因为它没有盈利(使用-fvect cost model=unlimited来查看它将产生什么),因为它严重高估了开场白成本(它构建向量{a,b}至少3次,{1,1}两次,等等)。对于llvm获得的testfunc_平面的SSE矢量化,但不是gcc。
// 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);
}