AVX2代码慢于没有AVX2

时间:2018-08-14 05:42:25

标签: c++ performance x86 avx2

我一直不太幸运地尝试着开始使用AVX2指令(this功能列表很有帮助)。最后,我得到了我的第一个程序,并按照自己的意愿进行了编译。我必须执行的程序需要两个u_char,并且将其加倍。本质上,我用它来解码来自摄像机的u_char数组中存储的数据,但我认为与这个问题无关。

获取两个double中的u_char的过程是:

double result = sqrt(double((msb<<8) + lsb)/64);

其中msblsb是两个u_char变量,其变量的最高有效位(msb)和最低有效位(lsbdouble进行计算。数据存储在代表行主要矩阵的数组中,其中值编码列msb的{​​{1}}和lsb分别位于第二行和第三行中。我已经在有和没有AVX2的情况下对此进行了编码:

i

但是,令我惊讶的是,此代码比普通代码慢吗?关于如何加快速度的任何想法?

我正在使用具有以下选项void getData(u_char* data, size_t cols, std::vector<double>& info) { info.resize(cols); for (size_t i = 0; i < cols; i++) { info[i] = sqrt(double((data[cols + i] << 8) + data[2 * cols + i]) / 64.0); ; } } void getDataAVX2(u_char* data, size_t cols, std::vector<double>& info) { __m256d dividend = _mm256_set_pd(1 / 64.0, 1 / 64.0, 1 / 64.0, 1 / 64.0); info.resize(cols); __m256d result; for (size_t i = 0; i < cols / 4; i++) { __m256d divisor = _mm256_set_pd(double((data[4 * i + 3 + cols] << 8) + data[4 * i + 2 * cols + 3]), double((data[4 * i + 2 + cols] << 8) + data[4 * i + 2 * cols + 2]), double((data[4 * i + 1 + cols] << 8) + data[4 * i + 2 * cols + 1]), double((data[4 * i + cols] << 8) + data[4 * i + 2 * cols])); _mm256_storeu_pd(&info[0] + 4 * i, _mm256_sqrt_pd(_mm256_mul_pd(divisor, dividend))); } } 的{​​{1}}(7.3.0)进行编译。我已经按照here的说明进行了检查,并且我的CPU(英特尔®酷睿™i7-4710HQ CPU @ 2.50GHz)支持AVX2。

要检查哪个速度更快,请使用时间。以下函数为我提供了时间戳:

c++

我在每个函数-std=c++17 -Wall -Wextra -O3 -fno-tree-vectorize -mavx2inline double timestamp() { struct timeval tp; gettimeofday(&tp, nullptr); return double(tp.tv_sec) + tp.tv_usec / 1000000.; } 之前和之后获取时间戳,并将它们相减以获得每个函数的经过时间。总体getData如下:

getDataAVX2

完整示例见here

1 个答案:

答案 0 :(得分:5)

在定时间隔内进行如此少量的工作很难准确测量。 cols = 80只有20个__m256d向量。

您的Skylake系统上的测试程序有时会在9.53674e-07 s1.19209e-06 s0 s之间跳动,而AVX2版本通常会更快。 (我在另一个内核上运行了_mm_pause()繁忙循环,以最大速度固定所有内核。这是台式机i7-6700k,因此所有内核共享相同的内核时钟频率。)

gettimeofday显然不够精确,无法测量任何短的东西。 struct timeval使用秒和 micro 秒,而不是纳秒。但是,我确实一致地看到,使用g++ -O3 -march=native编译的AVX2版本在Skylake上速度更快。我没有Haswell可以测试。我的Skylake使用的是硬件P状态电源管理,因此即使我不提前固定CPU频率,它也会迅速提升到最大非常。 Haswell没有该功能,因此这是您的事物会变得很奇怪的另一个原因。

如果要测量挂钟时间(instead of core clock cycles),请像普通人一样使用std::chronoCorrect way of portably timing code using C++11


预热效果将占主导地位,并且您将std::vector::resize()包含在定时间隔内。两个不同的std::vector<double>对象必须分别分配内存,因此第二个对象可能需要从OS获取新页面并花费更长的时间。如果main之前的内容(或cout <<中的内容)进行了一些临时分配,然后缩小或释放了它,则也许第一个可以从空闲列表中获取内存。

这里有很多可能性:首先,有人报告说,在Haswell like Agner Fog measured on Skylake上的前几微秒中,看到256位矢量指令的运行速度较慢。

可能是CPU决定在第二个定时间隔(AVX2间隔)期间将其最大加速时间提升到。在i7-4700MQ(2.4GHz Haswell)上可能需要20k时钟周期。 (Lost Cycles on Intel? An inconsistency between rdtsc and CPU_CLK_UNHALTED.REF_TSC)。

也许在write系统调用(来自cout <<之后),TLB丢失或分支丢失对第二个函数的伤害更大吗? (在内核中启用了Spectre + Meltdown缓解功能后,您应该期望代码从系统调用返回后运行缓慢。)

由于您没有使用-ffast-math,因此GCC不会将标量sqrt转换为rsqrtss的近似值,尤其是因为它是double而不是{{1} }。否则可以解释它。


看看时间如何随着问题的大小而变化,以确保您的微基准测试是合理的,除非您尝试来测量瞬态/热身效果,否则请重复很多次。如果没有优化,只需在定时间隔内在函数调用周围打一个重复循环即可(而不是尝试从多个间隔中累加时间)。检查编译器生成的asm,或者至少检查时间是否与重复计数成线性比例。您可以将函数float用作击败优化器的方法,使其无法在重复循环迭代中进行优化。


除预热效果外,SIMD版本的速度应约为Haswell标量的2倍

即使合并到__attribute__((noinline,noclone))之前输入的标量计算效率低下,标量和SIMD版本在除法单元上都存在瓶颈。 Haswell的FP分频/ sqrt硬件只有128位宽(因此__m256d被分成两个128位)。但是标量仅利用了一半的吞吐量。

vsqrtpd ymm将使您的吞吐量提高4倍:每个SIMD向量的元素数量增加一倍,float(压缩后的单个)的吞吐量是vsqrtps(压缩后的两倍)的两倍在Haswell。 (https://agner.org/optimize/)。它还可能更容易使用vsqrtpd作为x * approx_rsqrt(x)的快速近似值,可能是通过Newton-Raphson迭代将精度从〜12位提高到〜24(几乎与{{1 }})。 参见Fast vectorized rsqrt and reciprocal with SSE/AVX depending on precision。 (如果您有足够的工作要在不限制分频器吞吐量的同一循环中完成,那么实际的sqrt指令可能会很好。)

如果确实需要将输出格式设置为sqrt(x),则可以将_mm256_sqrt_pssqrt进行SIMD,然后然后转换为float与您的其余代码兼容


优化除sqrt以外的其他内容

在Haswell上这可能不会更快,但是如果其他线程不使用SQRT / DIV,则可能对超线程更友好。

它使用SIMD加载和解压缩数据double最好是将doublea<<8 + b中的字节交织成16位整数,与b。然后零扩展为32位整数,因此我们可以使用SIMD a-> _mm_unpacklo/hi_epi8转换。

对于每对int的数据,这将产生double的4个向量。在这里使用256位向量只会带来车道交叉问题,并且由于double的工作原理而需要提取到128个。

我改为直接在输出中使用__m128i,而不是希望gcc可以优化一次一次元素分配。

我还注意到编译器在每次存储后都重新加载_mm256_cvtepi32_pd(__m128i),因为其别名分析不能证明_mm256_storeu_pd只是在修改矢量数据,而不是在修改控制块。因此,我将基地址分配给了&info[0]局部变量,编译器确定该局部变量没有指向自身。

_mm256_storeu_pd

compiles to a pretty nice loop on the Godbolt compiler explorerdouble*

要处理#include <immintrin.h> #include <vector> inline __m256d cvt_scale_sqrt(__m128i vi){ __m256d vd = _mm256_cvtepi32_pd(vi); vd = _mm256_mul_pd(vd, _mm256_set1_pd(1./64.)); return _mm256_sqrt_pd(vd); } // assumes cols is a multiple of 16 // SIMD for everything before the multiple/sqrt as well // but probably no speedup because this and others just bottleneck on that. void getDataAVX2_vector_unpack(const u_char*__restrict data, size_t cols, std::vector<double>& info_vec) { info_vec.resize(cols); // TODO: hoist this out of the timed region double *info = &info_vec[0]; // our stores don't alias the vector control-block // but gcc doesn't figure that out, so read the pointer into a local for (size_t i = 0; i < cols / 4; i+=4) { // 128-bit vectors because packed int->double expands to 256-bit __m128i a = _mm_loadu_si128((const __m128i*)&data[4 * i + cols]); // 16 elements __m128i b = _mm_loadu_si128((const __m128i*)&data[4 * i + 2*cols]); __m128i lo16 = _mm_unpacklo_epi8(b,a); // a<<8 | b packed 16-bit integers __m128i hi16 = _mm_unpackhi_epi8(b,a); __m128i lo_lo = _mm_unpacklo_epi16(lo16, _mm_setzero_si128()); __m128i lo_hi = _mm_unpackhi_epi16(lo16, _mm_setzero_si128()); __m128i hi_lo = _mm_unpacklo_epi16(hi16, _mm_setzero_si128()); __m128i hi_hi = _mm_unpackhi_epi16(hi16, _mm_setzero_si128()); _mm256_storeu_pd(&info[4*(i + 0)], cvt_scale_sqrt(lo_lo)); _mm256_storeu_pd(&info[4*(i + 1)], cvt_scale_sqrt(lo_hi)); _mm256_storeu_pd(&info[4*(i + 2)], cvt_scale_sqrt(hi_lo)); _mm256_storeu_pd(&info[4*(i + 3)], cvt_scale_sqrt(hi_hi)); } } 不是16的倍数,您将需要循环的另一版本,或填充或其他内容。

但是,除g++ -O3 -march=haswell以外的其他指令很少,根本无法解决该瓶颈。

According to IACA,位于分隔器单元Haswell瓶颈上的所有SIMD循环,每个cols 28个循环,甚至是您的原始标量也要执行大量标量工作。 28个周期是很长的时间。

对于较大的输入,Skylake的速度应该提高一倍以上,因为它可以提高分频器的吞吐量。但是vsqrtpd仍然可以达到约4倍的速度,或者vsqrtpd ymm可以达到更高的速度。