如何在简单的循环中达到AVX计算吞吐量

时间:2015-05-04 21:49:27

标签: c++ optimization simd intrinsics avx

最近我正在通过有限差分法研究计算电动力学的数值求解器。

求解器的实现非常简单,但很难达到现代处理器的理论吞吐量,因为加载的数据只有1个数学运算,例如:

    #pragma ivdep
    for(int ii=0;ii<Large_Number;ii++)
    { Z[ii]  = C1*Z[ii] + C2*D[ii];}

Large_Number约为1,000,000,但不大于10,000,000

我试图手动展开循环并编写AVX代码,但未能加快速度:

int Vec_Size    = 8;
int Unroll_Num  = 6;
int remainder   = Large_Number%(Vec_Size*Unroll_Num);
int iter        = Large_Number/(Vec_Size*Unroll_Num);
int addr_incr   = Vec_Size*Unroll_Num;

__m256 AVX_Div1, AVX_Div2, AVX_Div3, AVX_Div4, AVX_Div5, AVX_Div6;
__m256 AVX_Z1,   AVX_Z2,   AVX_Z3,   AVX_Z4,   AVX_Z5,   AVX_Z6;

__m256 AVX_Zb = _mm256_set1_ps(Zb);
__m256 AVX_Za = _mm256_set1_ps(Za);
for(int it=0;it<iter;it++)
{
    int addr    = addr + addr_incr;
    AVX_Div1    = _mm256_loadu_ps(&Div1[addr]);     
    AVX_Z1      = _mm256_loadu_ps(&Z[addr]);
    AVX_Z1      = _mm256_add_ps(_mm256_mul_ps(AVX_Zb,AVX_Div1),_mm256_mul_ps(AVX_Za,AVX_Z1));
    _mm256_storeu_ps(&Z[addr],AVX_Z1);

    AVX_Div2    = _mm256_loadu_ps(&Div1[addr+8]);
    AVX_Z2      = _mm256_loadu_ps(&Z[addr+8]);
    AVX_Z2      = _mm256_add_ps(_mm256_mul_ps(AVX_Zb,AVX_Div2),_mm256_mul_ps(AVX_Za,AVX_Z2));
    _mm256_storeu_ps(&Z[addr+8],AVX_Z2);

    AVX_Div3    = _mm256_loadu_ps(&Div1[addr+16]);
    AVX_Z3      = _mm256_loadu_ps(&Z[addr+16]);
    AVX_Z3      = _mm256_add_ps(_mm256_mul_ps(AVX_Zb,AVX_Div3),_mm256_mul_ps(AVX_Za,AVX_Z3));
    _mm256_storeu_ps(&Z[addr+16],AVX_Z3);

    AVX_Div4    = _mm256_loadu_ps(&Div1[addr+24]);
    AVX_Z4      = _mm256_loadu_ps(&Z[addr+24]);
    AVX_Z4      = _mm256_add_ps(_mm256_mul_ps(AVX_Zb,AVX_Div4),_mm256_mul_ps(AVX_Za,AVX_Z4));
    _mm256_storeu_ps(&Z[addr+24],AVX_Z4);

    AVX_Div5    = _mm256_loadu_ps(&Div1[addr+32]);
    AVX_Z5      = _mm256_loadu_ps(&Z[addr+32]);
    AVX_Z5      = _mm256_add_ps(_mm256_mul_ps(AVX_Zb,AVX_Div5),_mm256_mul_ps(AVX_Za,AVX_Z5));
    _mm256_storeu_ps(&Z[addr+32],AVX_Z5);

    AVX_Div6    = _mm256_loadu_ps(&Div1[addr+40]);
    AVX_Z6      = _mm256_loadu_ps(&Z[addr+40]);
    AVX_Z6      = _mm256_add_ps(_mm256_mul_ps(AVX_Zb,AVX_Div6),_mm256_mul_ps(AVX_Za,AVX_Z6));
    _mm256_storeu_ps(&Z[addr+40],AVX_Z6);
}

上面的AVX循环实际上比Inter编译器生成的代码慢一点。

编译器生成的代码可以达到大约8G flops / s,大约是3GHz Ivybridge处理器的单线程理论吞吐量的25%。我想知道是否有可能达到像这样的简单循环的吞吐量。

谢谢!

3 个答案:

答案 0 :(得分:1)

您确定8 GFLOPS / s约为3 GHz Ivybridge处理器最大吞吐量的25%吗?我们来做计算吧。

每8个元素需要两个单精度AVX乘法和一个AVX加法。 Ivybridge处理器每个周期只能执行一个8宽AVX加法和一个8宽AVX乘法。此外,由于加法取决于两次乘法,因此需要3个周期来处理8个元素。由于加法可以与下一个乘法重叠,我们可以将其减少到每8个元素2个周期。对于十亿个元素,需要2 * 10 ^ 9/8 = 10 ^ 9/4个周期。考虑到3 GHz时钟,我们得到10 ^ 9/4 * 10 ^ -9 / 3 = 1/12 = 0.08秒。因此,最大理论吞吐量为12 GLOPS / s,编译器生成的代码达到66%,这很好。

还有一件事,通过将循环展开8次,可以有效地进行矢量化。我怀疑如果你展开这个特定的循环,你会获得任何显着的加速,特别是超过16次。

答案 1 :(得分:1)

我认为真正的瓶颈是每2次乘法和1次加法有2个加载和1个存储指令。也许计算是内存带宽有限。每个元素都需要传输12个字节的数据,如果每秒处理2G元素(即6G触发器)即24GB / s数据传输,则达到常春藤网桥的理论带宽。我想知道这个论点是否成立,并且确实没有解决这个问题的方法。

我回答自己的问题的原因是希望有人能够在我轻易放弃优化之前纠正我。这个简单的循环对于许多科学求解器来说非常重要,它是有限元和有限差分方法的支柱。如果一个人甚至无法提供一个处理器因为计算内存带宽有限,为什么还要烦扰多核呢?高带宽GPU或Xeon Phi应该是更好的解决方案。

答案 2 :(得分:1)

提高像你这样的代码的性能是很好的探索&#34;仍然是受欢迎的地区。看一下dot-product(Z Boson提供的完美链接)或某些(D)AXPY优化讨论(https://scicomp.stackexchange.com/questions/1932/are-daxpy-dcopy-dscal-overkills

一般而言,探索和考虑应用的关键主题是:

  • AVX2优于AVX,因为 FMA 和更好的加载/存储端口u-architecture on Haswell
  • 预取。 &#34;流媒体商店&#34;,&#34;非临时商店&#34;对于某些平台。
  • 线程并行以达到最大持续带宽
  • 展开(已由您完成;英特尔编译器也可以使用#pragma unroll(X)执行此操作)。 &#34;流媒体&#34;没有太大区别码。
  • 最后决定您希望优化代码的一组硬件平台

最后一颗子弹特别重要,因为对于&#34;流媒体&#34;和整体内存绑定代码 - 了解目标内存 - sybsystems的更多信息非常重要;例如,对于现有的,特别是未来的高端HPC服务器(代号为Knights Landing的第二代Xeon Phi),您可能会有一个非常不同的车顶线模型&#34;平衡带宽和计算之间的平衡,甚至是针对普通台式机优化的技术。