我建议制作基于 tayler 系列的 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 );
}
对于 4 个组件的计算,我可以获得 57~58 个 CPU 周期。
我搜索了谷歌并进行了一些测试以检查我的 sin/cos 的准确性。有些文章说 10 次迭代是双精度精确的,而 -M_PI/2 < angle < +M_PI/2。而且我的测试结果表明它比 math.h 在 -M_PI < 角度 < +M_PI 范围内的 sin/cos 更准确。如果需要,您可以增加迭代以提高大角度的准确性。
但是,我将更深入地优化此代码。此代码有延迟问题计算 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 个分量计算。(102~103 循环为 8 个组件)
它消除了泰勒计算循环中的乘法延迟,并使用了 85% 的 AVX 乘法单元。展开将解决许多延迟问题,但它不会将寄存器交换到内存。编译时生成 asm 文件,看看你的编译器如何处理你的代码。我尝试展开更多,但结果很糟糕,因为它无法容纳 16 个 AVX 寄存器。
现在我们使用内存优化。将 _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 提示缓存以加快读取速度。我建议根据物理内存的 RAS-CAS 延迟在 400~500 个 CPU 周期之前进行预取。在最坏的情况下,物理内存延迟最多可能需要 300 个周期。可能因硬件配置而异,即使使用昂贵的低 RAS-CAS 延迟内存也不会小于 200 个周期。
0.064 秒(计数 = 18562320)
sin/cos 优化结束。:-)