【问题标题】:Parallelizing a nested for loop: parcelling out data并行化嵌套的 for 循环:分割数据
【发布时间】:2021-02-09 02:01:40
【问题描述】:

我有两个带整数参数的函数;称它们为 fg。我还有另一个函数 h 采用两个整数参数。给定一个大小为 D 的正方形 U(意思是:{m0,m0+1,..,m0+D-1}x{n0,n0+1,...,n0+D-1}),我有一个程序用于计算 f(n) g(m) h(n,m) 在时间上在 D 上大致线性的总和,给定数组 farr, garr 包含 f(m0),f(m0+1),.. .,f(m0+D-1) 和 g(n0),g(n0+1),...,g(n0+D-1);让我们将该过程视为一个黑匣子,例如,我们通过调用 Sum(farr,garr,m0,n0,D) 来调用它。我们可以计算 farr[0]=f(m0),...farr[D-1]=f(m0+D-1) 或 garr[0]=garr(n0),garr[1]=g(n0 +1),...,garr[n0+D-1] 通过调用 Fillf(f,m0,D) 和 Fillg(g,m0,D) 在时间上大致呈线性关系。

问题是如何有效计算 {0,1,...,rD-1}x{ 中所有 (n,m) 的 f(n) g(m) h(n,m) 和0,1,...,rD-1}(比如说​​)并行。这在抽象上很容易——我想知道的是如何在 OpenMP 中做到这一点。

最简单的方法可能是这样的:

S=0;
#pragma omp parallel for collapse(2) schedule(dynamic) private(m0,n0,farr,garr) reduction(+:S)
for(m0=0; m0<r*D; m0+=D)
 for(n0=0; n0<r*D; n0+=D)
  farr = (short *) calloc(D,sizeof(int));
  Fillf(farr,m0,D);
  garr = (short *) calloc(D,sizeof(int));
  Fillg(garr,n0,D);
  S+=Sum(farr,garr,m0,n0,D)
  free(garr);
  free(farr);

这很好用,但它的缺点是farrgarr 的每个段都被计算r 次而不是一次。算不上悲剧,因为整体计算复杂度没有改变(不会比 O(r^2 D) 好),但还是不可取的。

另一种方法是写

S=0;
#pragma omp parallel for schedule(dynamic) private(m0,n0,farr,garr) reduction(+:S)
for(m0=0; m0<r*D; m0+=D) {
 farr = (short *) calloc(D,sizeof(int));
 Fillf(farr,m0,D);
 for(n0=0; n0<r*D; n0+=D) {
  garr = (short *) calloc(D,sizeof(int));
  Fillg(garr,n0,D);
  S+=Sum(farr,garr,m0,n0,D)
  free(garr);
 }
 free(farr);
}
  

这也是一个可行的解决方案,但是:(a) garr 的每个段仍然被计算 r 次而不是一次,(b) 如果可用线程的数量远大于 r,则并行化将效率低下(但小于r^2)。这里我们不能使用collapse(2),因为两个循环之间有一些事情发生。

显然应该可以做得更好。使用 OpenMP 对或多或少明显的过程进行编码的直接方法是什么? (应该预先计算大小约为 sqrt(s) D 的 farrgarr 的段,其中 s 是可用线程的数量,然后使用 collapse(2) 进行嵌套循环以获取 m0n0在大约 D sqrt(s) 的长度段上?)

【问题讨论】:

    标签: c++ c openmp nested-loops


    【解决方案1】:

    如果您想避免farrgarr 的重复计算,那么您至少有两种选择:

    1. 提前计算并存储所有garr,在一个单独的循环中,并采用与您的第二种选择相同的方法。或者,也可以提前计算并存储所有 farr

    2. 修改您的第二个替代方案以并行化内循环而不是外循环。这也将使您能够将farr 的分配和释放提升到循环之外:

      S = 0;
      farr = malloc(D, sizeof(int));
      for (int m0 = 0; m0 < r * D; m0 += D) {
          int S2 = 0;
          Fillf(farr, m0, D);
          #pragma omp parallel for private(farr, m0) reduction(+:S2)
          for (int n0 = 0; n0 < r * D; n0 += D) {
              int *garr = calloc(D, sizeof(int));
              Fillg(garr, n0, D);
              S2 += Sum(farr, garr, m0, n0, D)
              free(garr);
          }
          S += S2;
      }
      free(farr);
      

    请注意,

    • 如果Fillf() 假定它的数组参数最初是零填充的(正如calloc() 所确保的那样),那么将内存分配提升到循环之外将需要在每次调用之前手动填充零,但这仍然有可能比重新分配和重新分配便宜。
    • 我删除了调度子句,因为动态调度似乎对这种计算没有任何好处
    • 声明farr 私有只会使指针私有,而不是它指向的数据。此外,由于farrm0 都没有在并行循环内被修改,因此将它们声明为私有实际上并没有什么用处。我在示例代码中留下了private 子句,主要是作为这些评论的重点。
    • 重新计算garr 的各种值所节省的成本与细粒度并行带来的额外开销相抵触。如果内部循环的每次迭代的工作量仍然比较大,这更有可能是胜利。

    【讨论】:

    • 谢谢。 1. 并不是一个真正的选择,因为空间复杂度可能会上升。以大小为 sqrt(s) D 的块计算 f 和 g 不是更有意义吗,其中 s 是线程数?另外,为什么第二种选择优于第一种选择?
    • 我实际上尝试了您的选项 2。首先。问题再次存在(在我看来)r 可能远小于线程数。 (假设 r^2 至少与线程数一样大,这样我们就有希望有效地使用资源。)事实上,在上面列出的选项中,对我的特定应用程序运行最快的是第一个(“最简单的方法”)即使它多次计算 f 和 g 的每个值。
    • 我认为在最内层循环中声明 int n0, int *garr 与将它们设为私有具有相同的效果?
    • 最后 - 如果计算 f(n) g(m) h(n,m) 的总和在某些方格中比在其他方格中花费更多的时间,动态调度会有所帮助吗?当然,任何通过拆分为侧 sqrt(s) D 的部分来提高程序效率的尝试都会受到影响,所以我们不妨假设情况并非如此。
    • @HAHelfgott,是的,预先计算所有数据可能会对存储提出比您想要的更高的要求。这是一个时间vs。空间权衡。但是如果你并行化外循环,不管有没有collapse(2),并使用多个线程来执行它,那么我看不出有其他方法可以避免garr的重复计算。毕竟,这就是问题所在。这种重复生成更快的代码并不是不可能的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多