sta*_*rdt 4 gcc sse clang 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(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);
}
Run Code Online (Sandbox Code Playgroud)
GCC 4.7.2(带有-O3 -mavx)对循环版本进行矢量化处理,但对展开的循环使用标量运算。三种版本的(标准化)时间分别为3.3(循环,自动矢量化),1.2(展开,标量),1(手动avx)。展开版本和手动矢量化功能之间的性能差异很小,但是我想强制矢量化,因为它在完整代码中很有用。
使用不同的编译器进行测试(请参阅https://godbolt.org/g/HJH2CX)显示,clang自动对展开的循环进行矢量化处理(从3.4.1版开始),而GCC直到7版都没有。我可以使用GCC自动获得类似的矢量化吗?我只发现与循环矢量化有关的优化选项没有帮助。自2011年以来,海湾合作委员会网站没有任何新闻。
gcc通常不会向量化单个向量的东西。我已经Point{ double x,y; }在现有代码库(Endless Sky)中看到了类似的缺少自动矢量化的类。
因此,如果需要x86内联到快速代码,则可能必须手动对其进行矢量化处理。(您也可以考虑传递__m256d值而不是存储到数组。)
顺便说一句,手动矢量化版本可能会更快。我在Godbolt上玩过它,发现它_mm256_set_pd(-b,-a, b,a)正在编译为愚蠢的代码,因此手动进行处理会更高效。另外,如果没有可用的FMA,则可以通过重构表达式来减少延迟。(允许0.1-/ + a与平方同时发生)。 代码+汇编在这里
// 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);
}
Run Code Online (Sandbox Code Playgroud)
IDK为什么测试时自动向量化的循环如此慢?它确实将标量存储到数组中,然后进行向量加载,导致〜11个周期的存储转发停顿。因此它的延迟要比其他两种方法高得多,但是IDK是否会影响吞吐量。IDK您的测试方式如何;也许您将一个呼叫的结果用作下一个呼叫的输入?还是在相同堆栈空间的同一块上重复进行存储转发停滞是一个问题?
通常,对于较大的数组,gcc确实喜欢将指针对齐。它生成巨大的完全展开的标量内/外代码以到达对齐的指针,然后使用对齐的存储/加载。
这对于现代CPU并没有多大帮助(但通常也不会造成太大的伤害),尤其是对于通常在运行时对齐的数据,但是如果数据通常是未对齐的或在Nehalem之前运行的话,则可能会很好中央处理器。
IDK是否与gcc不愿自动矢量化小事物有关,但告诉它double*对齐似乎并没有帮助。
我认为问题的部分原因在于,它不能很好地插入洗牌来向量化需要洗牌的代码。