我在C
中有一个简单的循环,我将magnitude
和angle
转换为real
和imaginary
部分。我有两个版本的循环。 Version 1
是一个简单的for循环,我使用以下代码执行转换
for(k = 0; k < n; k++){
xReal[k] = Mag[k] * cos(Angle[k]);
xImag[k] = Mag[k] * sin(Angle[k]);
}
Version 2
其中Intrinsics
用于对循环进行矢量化。
__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
使用这些值进行的简单计算表明,在version 1
中,每次迭代需要几乎36
个周期才能完成,而117
周期需要Version 2
个周期才能完成。考虑到sine
和cosine
函数的计算自然很昂贵,这些数字似乎并不可怕。但是,这个循环是我的功能的一个严重瓶颈,因为分析显示几乎1/3
的时间花在循环中。所以,我想知道是否有任何方法可以加快这个循环(例如,以不同的方式计算sine
和cosine
函数)。如果能帮助我解决这个问题,请告诉我是否有改进此循环性能的空间。
提前感谢您的帮助
PS:我正在使用icc
来编译代码。另外,我应该提到数据没有对齐(也不可能)。但是,对齐数据只会导致性能略有改善(低于1%)。
答案 0 :(得分:6)
我建议使用基于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。角度&lt; + M_PI / 2。我的测试结果显示它比math.h的sin / cos更准确 - -M_PI&lt;角度&lt; + 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周期。繁忙的AVX乘法单元占94%。
Prefech提示缓存以便更快地读取。我建议在400~500 CPU周期之前根据物理内存的RAS-CAS延迟进行预先优化。在最坏的情况下,物理内存延迟最多可能需要300个周期。可能因硬件配置而异,即使使用昂贵的低RAS-CAS延迟存储器,也不会小于200个周期。
0.064秒(计数= 18562320)
sin / cos优化结束。 : - )
答案 1 :(得分:1)
请检查:
数组的起始地址是否与16byte对齐。 i7支持高延迟未对齐avx负载而不会抱怨“总线错误”
请使用配置文件工具检查缓存命中率和未命中率。内存访问似乎是循环版本2的瓶颈
您可以降级精度,或使用结果表进行sin和cos计算。
请考虑您计划实现的性能提升程度。由于循环的版本1仅占总运行时间的1/3。如果将循环优化为零,则性能仅提高30%
答案 2 :(得分:0)
您列出的计时结果显示,与版本1相比,版本2运行速度更快(20%)。
Version 1: n = 18562320, Time: 0.2sec
Version 2: n = 18562320, Time: 0.16sec
不确定如何计算每个版本中使用的周期?处理器中正在进行大量工作,并且缓存提取可能会导致时间差异,即使v1使用较少的周期(再次不知道您如何计算周期)。
或者另一种解释方法是,通过向量化,数据元素可用,而无需等待内存提取。