我一直不太幸运地尝试着开始使用AVX2指令(this功能列表很有帮助)。最后,我得到了我的第一个程序,并按照自己的意愿进行了编译。我必须执行的程序需要两个u_char
,并且将其加倍。本质上,我用它来解码来自摄像机的u_char数组中存储的数据,但我认为与这个问题无关。
获取两个double
中的u_char
的过程是:
double result = sqrt(double((msb<<8) + lsb)/64);
其中msb
和lsb
是两个u_char
变量,其变量的最高有效位(msb
)和最低有效位(lsb
) double
进行计算。数据存储在代表行主要矩阵的数组中,其中值编码列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 -mavx2
和inline double timestamp()
{
struct timeval tp;
gettimeofday(&tp, nullptr);
return double(tp.tv_sec) + tp.tv_usec / 1000000.;
}
之前和之后获取时间戳,并将它们相减以获得每个函数的经过时间。总体getData
如下:
getDataAVX2
完整示例见here。
答案 0 :(得分:5)
在定时间隔内进行如此少量的工作很难准确测量。 cols = 80
只有20个__m256d
向量。
您的Skylake系统上的测试程序有时会在9.53674e-07 s
,1.19209e-06 s
和0 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::chrono
。 Correct 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_ps
与sqrt
进行SIMD,然后然后转换为float
与您的其余代码兼容。
优化除sqrt以外的其他内容:
在Haswell上这可能不会更快,但是如果其他线程不使用SQRT / DIV,则可能对超线程更友好。
它使用SIMD加载和解压缩数据:double
最好是将double
和a<<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 explorer和double*
。
要处理#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
可以达到更高的速度。