一些mandelbrot绘制例程从c到sse2

时间:2013-04-13 09:38:37

标签: c optimization x86 sse mandelbrot

我想将这样简单的例程重写为SSE2代码,(最好是 在nasm),我不完全确定如何做,两件事 不清楚(如何表达计算(内循环和来自 外循环)以及如何调用c代码函数“SetPixelInDibInt(i,j,palette [n]);” 来自静态链接的asm代码

    void DrawMandelbrotD(double ox, double oy, double lx, int N_ITER)
    {
     double ly = lx * double(CLIENT_Y)/double(CLIENT_X);
     double dx = lx / CLIENT_X;
     double dy = ly / CLIENT_Y;
     double ax = ox - lx * 0.5 + dx * 0.5;
     double ay = oy - ly * 0.5 + dy * 0.5;
    static  double re, im, re_n, im_n, c_re, c_im, rere, imim, int n;

    for(int j=0; j<CLIENT_Y; j+=1)
    {
     for(int i=0; i<CLIENT_X; i+=1)
     {
      c_re = ax + i * dx;
      c_im = ay + j * dy;
      re = c_re;
      im = c_im;
      rere=re*re;
      imim=im*im;
      n=1;

      for(int k=0;k<N_ITER;k++)
      {
        im =  (re+re)*im    + c_im;
        re =   rere - imim  + c_re;
        rere=re*re;
        imim=im*im;
        if ( (rere + imim) > 4.0 ) break;
        n++;
       }
        SetPixelInDibInt(i ,j, palette[n]);
      }
     }
    }

有人可以提供帮助,我不想看到其他代码 实现,但只是上面那些的nasm-sse翻译 - 在我的情况下,这将是最有帮助的 - 有人可以帮忙吗?

1 个答案:

答案 0 :(得分:2)

英特尔作为AVX示例有完整的实现。见下文。

使Mandelbrot变得棘手的原因是集合中每个点(即像素)的早期条件是不同的。您可以保持一对或四个像素的迭代,直到两者的幅度都超过2.0(或者您达到最大迭代次数)。否则将需要跟踪哪个像素点在哪个向量元素中。

无论如何,在2(或4个AVX)的向量上操作的简单实现一次加倍将使其吞吐量受到依赖链的延迟的限制。您需要并行执行多个依赖链,以保持Haswell的两个FMA单元都能够进行馈送。因此,您要复制变量,并在内循环内对外循环的两次迭代进行交错操作。

跟踪正在计算的像素将会有点棘手。我认为对一行像素使用一组寄存器而对另一行使用另一组寄存器可能需要较少的开销。 (所以你总是可以向右移动4个像素,而不是检查另一个dep链是否已经在处理那个向量。)

我怀疑每4次迭代只检查循环退出条件可能是一次胜利。基于打包向量比较获取分支代码比在标量情况下稍微贵一些。额外的FP添加也是昂贵的。 (Haswell每个周期可以做两个FMA,(等待时间= 5)。单独的FP添加单元与其中一个FMA单元的端口相同。两个FP mul单元位于可以运行FMA的相同端口上。)

可以使用打包比较来检查循环条件,以生成零和1的掩码,并将该寄存器的(V)PTEST与其自身一起查看它是否全为零。 (编辑:movmskps然后test+jcc更少uops,但可能更高的延迟。)然后显然jejne视情况而定,具体取决于您是否进行了FP比较当你应该退出时为零,或者当你不应该退出时为零。 NAN不可能,但是没有理由不选择你的比较操作,这样NAN会导致退出条件为真。

const __mm256d const_four = _mm256_set1_pd(4.0);  // outside the loop

__m256i cmp_result = _mm256_cmp_pd(mag_squared, const_four, _CMP_LE_OQ);  // vcmppd.  result is non-zero if at least one element < 4.0
if (_mm256_testz_si256(cmp_result, cmp_result))
    break;

可能有某种方法可以直接在packed-double上使用PTEST,如果FP值为&gt;那么将使用一些bit-hack AND-mask选择将被设置的位。 4.0。就像指数中的一些比特一样?也许值得考虑。我发现了forum post,但没有尝试过。

嗯,哦,废话,这并没有为循环条件失败记录,对于每个向量元素分别,为了着色Mandelbrot集外的点。也许测试任何元素击中条件(而不是全部),记录结果,然后将该元素(和该元素的c)设置为0.0,这样它就赢了“t = t再次触发退出条件。或者可能将像素调度到矢量元素中是最重要的。这个代码在超线程CPU上可能做得相当不错,因为会有很多分支错误预测,每个元素分别触发早期状态。

这可能会浪费你的大量吞吐量,并且考虑到每个周期可以使用4个uop,但是其中只有2个可以是FP mul / add / FMA,因此需要大量的整数代码。安排点到矢量元素。 (在Sandybridge / Ivybrideg上,没有FMA,FP吞吐量较低。但是只有3个端口可以处理整数操作,其中2个是FP mul和FP添加单元的端口。)

由于您不必读取任何源数据,因此每个dep链只有1个内存访问流,而且它是一个写入流。 (而且它的带宽很低,因为大多数点在您准备写单个像素值之前需要进行大量迭代。)因此,硬件预取流的数量并不是数字的限制因素dep链并行运行高速缓存未命中延迟应该由写缓冲区隐藏。

如果有人仍然对此感兴趣(只是发表评论),我可以写一些代码。我停在高级设计阶段,因为这是一个老问题。

==============

我还发现英特尔已经使用Mandelbrot集作为其中一个AVX tutorials的示例。它们使用mask-off-vector-elements方法来处理循环条件。 (使用由vcmpps直接生成的掩码到AND)。他们的结果表明,AVX(单精度)比标量浮点数提高了7倍,因此显然相邻像素在非常不同的迭代次数下达到早期状态并不常见。 (至少对于他们测试过的缩放/平移。)

他们只是让FP结果不能累积到早期失败状态的元素。他们只是停止递增该元素的计数器。希望大多数系统默认将控制字设置为零非正规,如果非正规仍然需要额外的周期。

但是,它们的代码在某种程度上是愚蠢的:它们使用浮点向量跟踪每个向量元素的迭代计数,然后在使用之前将其转换为int。为了使用打包整数,它会更快,而不是占用FP执行单元。哦,我知道他们为什么这样做:AVX(没有AVX2)不支持256位整数向量操作。他们本可以使用压缩的16位int循环计数器,但这可能会溢出。 (并且他们必须将掩模从256b压缩到128b)。

他们还测试所有元素是&gt; 4.0使用movmskps然后测试,而不是使用ptest。我猜test / jcc可以宏融合,并且运行在与FP矢量操作不同的执行单元上,因此它可能不会更慢。哦,当然AVX(没有AVX2)没有256比特PTEST。此外,PTEST是2 uops,因此实际上movmskps + test / jcc的uop比ptest + jcc少。 (PTEST是SnB上的1个融合域uop,但执行端口仍有2个未融合的uop。在IvB / HSW上,即使在融合域中也是2 uops。)所以看起来movmskps是最佳方式,除非您可以利用按位AND PTEST的一部分,或者需要测试的不仅仅是每个元素的高位。如果分支不可预测,ptest可能会降低延迟,因此通过更快地捕获错误预测值是值得的。