【问题标题】:OpenMP false sharing and cache hits exploitationOpenMP 虚假共享和缓存命中利用
【发布时间】:2023-03-23 19:50:02
【问题描述】:

在我的代码中,有几种方法包含用于在 ny*nx 矩阵中移动的嵌套循环。我想并行化这个过程,所以我在每个方法上都使用了这样的东西:

#pragma omp parallel for private(jj,x_e,x_w,y_n,y_s)
  for(ii=0;ii<ny;ii++) {
    for(jj=0;jj<nx;jj++) {
      /* determine indices of axis-direction neighbours
      ** respecting periodic boundary conditions (wrap around) */
      y_n = (ii + 1) % ny;
      x_e = (jj + 1) % nx;
      y_s = (ii == 0) ? (ii + ny - 1) : (ii - 1);
      x_w = (jj == 0) ? (jj + nx - 1) : (jj - 1);
      //propagate densities to neighbouring cells, following
      tmp[ii *nx + jj].speeds[0]  = cells[ii*nx + jj].speeds[0]; /* central cell, */
                                                                                     /* no movement   */
      tmp[ii *nx + x_e].s[1] = cells[ii*nx + jj].s[1]; /* east */
      tmp[y_n*nx + jj].s[2]  = cells[ii*nx + jj].s[2]; /* north */
      tmp[ii *nx + x_w].s[3] = cells[ii*nx + jj].s[3]; /* west */
      tmp[y_s*nx + jj].s[4]  = cells[ii*nx + jj].s[4]; /* south */
      tmp[y_n*nx + x_e].s[5] = cells[ii*nx + jj].s[5]; /* north-east */
      tmp[y_n*nx + x_w].s[6] = cells[ii*nx + jj].s[6]; /* north-west */
      tmp[y_s*nx + x_w].s[7] = cells[ii*nx + jj].s[7]; /* south-west */      
      tmp[y_s*nx + x_e].s[8] = cells[ii*nx + jj].s[8]; /* south-east */      
    }
  }

但是,这段代码(以及其他代码)非常慢。有什么方法可以纠正我的#pragma 语句并重写数据结构或循环以使其缓存友好并避免错误共享?

PS:代码是用-O3 编译的,所以每次小优化都没有实现任何加速。

【问题讨论】:

  • tmpcells的原始数据结构是什么? nxny 有多大?
  • tmp 和单元格是相同的,它们是结构数组。每个结构都包含一个双精度数组 s(大小为 9)。在我的基本实验中,nx 和 ny 分别是 200 和 300,但它们可以达到 2000 和 3000

标签: c for-loop matrix parallel-processing openmp


【解决方案1】:

设置线程并将工作分配给它们会引入一些开销。由于你的工作量很小(200x300),而且工作很简单(只有一些数据副本),线程开销可能比你的实际工作还要大,这就是你没有得到性能提升的原因之一。

另一个原因是由于使用了结构数组 (AoS),您的代码缓存本地化很差。尤其是当您写信给tmp 时。为了获得更好的缓存命中性能,您可以考虑使用数组结构 (SoA)。实际上你只需要从

交换尺寸
cells[nx*ny][9]

cells[9][nx*ny]

然后你会发现部分复制可以直接用memcpy()完成。

另一方面,我不认为您的代码存在严重的错误共享问题。

【讨论】:

  • 虽然从 AoS 切换到 SoA 肯定会提高特定循环操作的速度,但它可能会对代码中未显示的其他部分产生负面影响。
  • 你可以考虑直接使用new double[9*nx*ny]
  • 我必须在 c 中这样做
  • 所以我创建了一个常规指针,但我以这种方式使用矩阵表示法 cells[9][nx*ny]
  • 如果我使用 malloc,我不能再使用 [][] 表示法,因为我得到一个错误“下标值既不是数组也不是指针也不是向量”。如果我使用单元格[9][nx*ny] 创建它,对于 30x20 版本来说它很好,但我会立即在 300x200 版本上遇到分段错误,即使我尝试在单元格 [0][0] 中写入
【解决方案2】:

错误共享发生在共享缓存行被修改时,即被写入。鉴于此,您可以通过简单地反转操作来大大改善代码的内存访问模式的局部性:而不是对cells 的每个元素执行分散操作,而是对tmp 的每个元素执行聚集操作:

tmp[ii*nx + jj].s[0] = cells[ii*nx + jj].s[0];
tmp[ii*nx + jj].s[1] = cells[...].s[1];
...
tmp[ii*nx + jj].s[8] = cells[...].s[8];

这样您将线性化每个线程的内存写入模式,使其对缓存更加友好,同时减少错误共享。

另外请注意,您的代码性能主要受内存带宽的限制,因此对于大型数组,您可能无法通过多个线程获得任何加速,除非这样做可以提供更多内存带宽,例如你有一个多插槽系统,每个插槽都有自己的内存控制器,每个线程在不同插槽的核心上执行。您使用 200 的 nx 和 300 的 ny 进行的测试已经需要至少 8.2 MiB 的内存,并且几乎不适合大多数桌面处理器的最后一级缓存,但仍然适合大多数服务器级 CPU 的缓存。更大的 2000 x 3000 案例肯定会受到内存限制。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-01-10
    • 1970-01-01
    • 1970-01-01
    • 2019-03-07
    • 2014-03-14
    • 2012-06-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多