for(k = 0; k < n; k++){ xReal[k] = Mag[k] * cos(Angle[k]); xImag[k] = Mag[k] * sin(Angle[k]); }
版本2,其中使用内在函数来对循环进行矢量化.
__m256d cosVec, sinVec; __m256d resultReal, resultImag; __m256d angVec, voltVec; for(k = 0; k < SysData->totNumOfBus; k+=4){ voltVec = _mm256_loadu_pd(volt + k); angVec = _mm256_loadu_pd(theta + k); sinVec = _mm256_sincos_pd(&cosVec, angVec); resultImag = _mm256_mul_pd(voltVec, sinVec); resultReal = _mm256_mul_pd(voltVec, cosVec); _mm256_store_pd(xReal+k, resultReal); _mm256_store_pd(xImag+k, resultImag); }
在Core i7 2600k @ 3.4GHz处理器上,这些循环给出以下结果:
Version 1: n = 18562320, Time: 0.2sec Version 2: n = 18562320, Time: 0.16sec
使用这些值的简单计算表明,在版本1中,每次迭代需要几乎36个周期才能完成,而完成版本2则需要117个周期.考虑到正弦和余弦函数的计算自然很昂贵,这些数字似乎并不可怕.然而,这个循环是我的功能的一个严重瓶颈,因为分析显示几乎1/3的时间花在循环中.所以,我想知道是否有任何方法可以加速这个循环(例如,以不同的方式计算正弦和余弦函数).如果能帮助我解决这个问题,请告诉我是否有空间来改善这个循环的性能.
在此先感谢您的帮助
PS:我正在使用icc来编译代码.另外,我应该提到数据没有对齐(也不可能).但是,对齐数据只会导致性能略有改善(低于1%).
我建议使用基于tayler series的sin / cos函数和_mm256_stream_pd()来存储数据.这是基本示例代码.__m256d sin_req[10]; __m256d cos_req[10]; __m256d one_pd = _mm256_set1_pd(1.0); for(int i=0; i<10; ++i) { sin_req[i] = i%2 == 0 ? _mm256_set1_pd(-1.0/Factorial((i+1)*2+1) ) : _mm256_set1_pd(+1.0/Factorial((i+1)*2+1) ); cos_req[i] = i%2 == 0 ? _mm256_set1_pd(-1.0/Factorial((i+1)*2+0) ) : _mm256_set1_pd(+1.0/Factorial((i+1)*2+0) ); } for(int i=0; i<count; i+=4) { __m256d voltVec = _mm256_load_pd(volt + i); __m256d angVec = _mm256_load_pd(theta + i); // sin/cos by taylor series __m256d angleSq = angVec * angVec; __m256d sinVec = angVec; __m256d cosVec = one_pd; __m256d sin_serise = sinVec; __m256d cos_serise = one_pd; for(int j=0; j<10; ++j) { sin_serise = sin_serise * angleSq; // [1] cos_serise = cos_serise * angleSq; sinVec = sinVec + sin_serise * sin_req[j]; cosVec = cosVec + cos_serise * cos_req[j]; } __m256d resultReal = voltVec * sinVec; __m256d resultImag = voltVec * cosVec; _mm256_store_pd(xReal + i, resultReal); _mm256_store_pd(xImag + i, resultImag ); }
我可以获得57~58个CPU周期来计算4个元件.
我搜索谷歌并进行了一些测试,以确定我的罪/ cos的准确性.一些文章说10次迭代是双精度准确的,而-M_PI / 2 <2.角度< M_PI / 2.我的测试结果显示它比math.h的sin / cos更准确 - -M_PI<角度< M_PI范围.如果需要,您可以增加迭代以获得更大的角度值. 但是我会更深入地优化这段代码.此代码具有延迟问题计算tayor系列. AVX的乘法延迟是5个CPU周期,这意味着我们不能以比5个周期更快的速度运行一次迭代,因为[1]使用了前一个迭代结果的结果. 我们可以像这样简单地展开它.
for(int i=0; i<count; i+=8) { __m256d voltVec0 = _mm256_load_pd(volt + i + 0); __m256d voltVec1 = _mm256_load_pd(volt + i + 4); __m256d angVec0 = _mm256_load_pd(theta + i + 0); __m256d angVec1 = _mm256_load_pd(theta + i + 4); __m256d sinVec0; __m256d sinVec1; __m256d cosVec0; __m256d cosVec1; __m256d angleSq0 = angVec0 * angVec0; __m256d angleSq1 = angVec1 * angVec1; sinVec0 = angVec0; sinVec1 = angVec1; cosVec0 = one_pd; cosVec1 = one_pd; __m256d sin_serise0 = sinVec0; __m256d sin_serise1 = sinVec1; __m256d cos_serise0 = one_pd; __m256d cos_serise1 = one_pd; for(int j=0; j<10; ++j) { sin_serise0 = sin_serise0 * angleSq0; cos_serise0 = cos_serise0 * angleSq0; sin_serise1 = sin_serise1 * angleSq1; cos_serise1 = cos_serise1 * angleSq1; sinVec0 = sinVec0 + sin_serise0 * sin_req[j]; cosVec0 = cosVec0 + cos_serise0 * cos_req[j]; sinVec1 = sinVec1 + sin_serise1 * sin_req[j]; cosVec1 = cosVec1 + cos_serise1 * cos_req[j]; } __m256d realResult0 = voltVec0 * sinVec0; __m256d imagResult0 = voltVec0 * cosVec0; __m256d realResult1 = voltVec1 * sinVec1; __m256d imagResult1 = voltVec1 * cosVec1; _mm256_store_pd(xReal + i + 0, realResult0); _mm256_store_pd(xImag + i + 0, imagResult0); _mm256_store_pd(xReal + i + 4, realResult1); _mm256_store_pd(xImag + i + 4, imagResult1); }
这个结果为51~51.5个循环,用于4分量计算. (8个组件102~103个循环)
它消除了泰勒计算循环中的多重延迟,并使用了85%的AVX乘法单位.展开将解决许多延迟问题,同时它不会将寄存器交换到内存.编译时生成asm文件,看看编译器如何处理代码.我尝试展开更多,但结果很糟糕,因为它不适合16个AVX寄存器.
现在我们选择内存optmize.将_mm256_store_ps()替换为_mm256_stream_ps().
_mm256_stream_pd(xReal + i + 0, realResult0); _mm256_stream_pd(xImag + i + 0, imagResult0); _mm256_stream_pd(xReal + i + 4, realResult1); _mm256_stream_pd(xImag + i + 4, imagResult1);
更换存储器写代码结果为48个周期,用于4分量计算.
如果你不打算回读,_mm256_stream_pd()总是更快.它跳过缓存系统并将数据直接发送到内存控制器,并且不会污染缓存.通过使用_mm256_stream_pd(),您将获得更多的数据总线/缓存空间来读取数据.
我们来试试预取.
for(int i=0; i<count; i+=8) { _mm_prefetch((const CHAR *)(volt + i + 5 * 8), _MM_HINT_T0); _mm_prefetch((const CHAR *)(theta + i + 5 * 8), _MM_HINT_T0); // calculations here. }
现在每次计算得到45.6~45.8个CPU周期. 94%忙于AVX乘法单元.
Prefech提示缓存以便更快地读取.我建议在400~500 CPU周期之前根据物理内存的RAS-CAS延迟进行预先优化.在最坏的情况下,物理内存延迟最多可能需要300个周期.可能因硬件配置而异,即使使用昂贵的低RAS-CAS延迟存储器,也不会小于200个周期.
0.064秒(计数= 18562320)
sin / cos优化结束.