在医学图像重建实施中改善局部性并减少缓存污染

时间:2011-01-18 21:44:33

标签: c caching optimization blocking simd

我正在为我的大学做一项与医学用图像重建算法相关的研究。

我遇到了长达3周的问题,我需要提高以下代码的性能:

for (lor=lor0[mypid]; lor <= lor1[mypid]; lor++)
{
  LOR_X = P.symmLOR[lor].x;
  LOR_Y = P.symmLOR[lor].y;
  LOR_XY = P.symmLOR[lor].xy;
  lor_z = P.symmLOR[lor].z;
  LOR_Z_X = P.symmLOR[lor_z].x;
  LOR_Z_Y = P.symmLOR[lor_z].y;
  LOR_Z_XY = P.symmLOR[lor_z].xy;  

  s0 = P.a2r[lor];
  s1 = P.a2r[lor+1];

  for (s=s0; s < s1; s++)
  {
    pixel     = P.a2b[s];
    v         = P.a2p[s]; 

    b[lor]    += v * x[pixel];

    p          = P.symm_Xpixel[pixel];
    b[LOR_X]  += v * x[p];

    p          = P.symm_Ypixel[pixel];
    b[LOR_Y]  += v * x[p];

    p          = P.symm_XYpixel[pixel];
    b[LOR_XY] += v * x[p];


    // do Z symmetry.
    pixel_z    = P.symm_Zpixel[pixel];
    b[lor_z]  += v * x[pixel_z];


    p          = P.symm_Xpixel[pixel_z];
    b[LOR_Z_X]  += v * x[p];


    p          = P.symm_Ypixel[pixel_z];
    b[LOR_Z_Y]  += v * x[p];

    p          = P.symm_XYpixel[pixel_z];
    b[LOR_Z_XY] += v * x[p];

   }

}

对于任何想要了解的人,该代码实现了MLEM转发功能,所有变量都是FLOAT

经过多次测试后,我注意到代码的这一部分出现了很大的延迟。 (你知道,90 - 10规则)。

后来,我使用Papi(http://cl.cs.utk.edu/papi/)来测量L1D缓存未命中。正如我所想的那样,Papi确认由于更多的未命中而导致性能下降,特别是对于随机访问b矢量(大小很大)。

在互联网上阅读信息我只知道到目前为止提高性能的两个选项:改善数据位置或减少数据污染。

为了进行第一次改进,我将尝试将代码更改为缓存感知,就像Ulrich Drepper在上提出的每个程序员应该知道的内存(www.akkadia。 org / drepper / cpumemory.pdf)A.1矩阵乘法。

我相信阻止SpMV(稀疏矩阵向量乘法)会提高性能。

另一方面,每次程序试图访问b vector时,我们都会遇到所谓的缓存污染

有没有办法在不使用缓存的情况下使用SIMD指令从b向量加载值?

此外,可以使用像void _mm_stream_ps(float * p,__ m128 a)这样的函数在向量b上存储一个浮点值而不会污染缓存吗?

我不能使用_mm_stream_ps因为总是存储4个浮点数,但是对b矢量的访问显然是随机的。

我希望在我的困境中明白。

更多信息:v是具有CRS格式的Sparse Matrix商店的列值。我意识到如果我尝试将CRS格式更改为其他格式,可以进行其他优化,但是,正如我之前所说,我已经进行了几个月的测试,我知道性能下降与向量b上的随机访问有关。从400.000.000 L1D未命中当我不存储在矢量b时,我可以转到100~未命中。

感谢。

4 个答案:

答案 0 :(得分:2)

减少向量b上的随机访问的简单优化是永远不要在内部for循环中写入向量b。

而是将向量B中所需的所有值加载到临时变量中,在更新这些临时变量时执行整个内部for循环,然后将临时变量写回向量B.

临时变量最好位于相同的缓存行上,具体取决于您的编译器和环境,您也可能提示编译器为这些变量使用寄存器。

答案 1 :(得分:2)

我甚至不会假装我知道代码在做什么:)但是一些额外内存访问的可能原因是别名:如果编译器无法确定b,{{1}并且各种x数组不重叠,然后写入P.symm将影响如何安排bx的读取。如果编译器特别悲观,它甚至可能强制从内存中重新获取P.symm的字段。所有这些都将导致您看到的缓存未命中。改善这种情况的两种简单方法是:

  1. P上使用__restrict。这可以保证b不与其他数组重叠,因此写入它不会影响其他数组的读取(或写入)。

  2. 对事物进行重新排序,以便首先从b读取所有内容,然后从P.symm读取,最后读取到x的所有内容。这应该打破读取中的一些依赖关系,并且编译器并行地调度b的读取,然后并行地从P.symm读取,并希望写入{{1}明智的。

  3. 另一个风格的东西(对第2点有帮助)就是不要重复使用变量x。没有理由你没有例如bpp_x等,这将使代码重新排序更容易。

    一旦完成,您就可以在已知的缓存未命中之前开始在gcc上播放预取指令(即p_y

    希望有所帮助。

答案 2 :(得分:2)

我会说,首先尝试帮助你的编译器。

  • 声明外部循环的边界 在循环之前为const
  • 声明您可能的所有变量 (所有LOR_..)作为局部变量, 就像是: float LOR_X = P.symmLOR[lor].x;size_t s0 = P.a2r[lor];
  • 这也特别适用于循环 变量,如果你碰巧有 现代,符合C99标准,编译器:for (size_t s=s0; s < s1; s++)
  • b单独加载和存储 向量。物品的位置 你访问,那里,不依赖 在s。所以创建一个局部变量 保持所有的实际价值 你处理的不同案件 内循环之前,更新内部的这些局部变量 循环,然后存储结果 内环。
  • 也许将你的内循环分开 一些。索引计算 相对便宜,然后你的 系统可能会更好地识别 流媒体访问您的一些 载体
  • 看看你的汇编程序 编译器生成并识别 内循环的代码。实验a 咬了很多“远”负荷 并存储在那个循环中。

编辑:重新阅读gravitron的答案和评论后,重要的是将变量声明为尽可能本地以检查汇编程序编译器是否成功拥有缓存 - 缺少内部循环外的载荷和存储。

答案 3 :(得分:0)

这些都是很好的答案,我会问为什么这么多索引?通过在本地变化不大的索引值?

此外,它不会杀死你做几个随机暂停,看看它通常是什么。