【问题标题】:For loop optimization in cc语言中的for循环优化
【发布时间】:2018-04-12 05:14:05
【问题描述】:

如何优化这个 for 循环:

for (l = 1; l <= loop; l++) {
    for (i = 1; i < n; i++) {
        x[i] = z[i] * (y[i] - x[i - 1]);
    }
}

我怎样才能通过OpenMp并行它的原始和优化版本?

【问题讨论】:

标签: c for-loop openmp


【解决方案1】:

假设你想并行化内循环

for ( i = 1; i < n; ++i ) {
    x[i] = z[i] * ( y[i] - x[i - 1] );
}

我建议预先计算不依赖于前一个循环的部分。更容易并行化。

double preComps [n];
#pragma omp parallel for
for( i = 1; i < n ; ++i ) {
    preComps[i] = z[i] * y[i];
}

// this loop is difficult to parallelize because of the data dependency on what was computed in the previous loop
for( i = 1; i < n ; ++i ) {
    x[i] = preComps[i] - z[i] * x[i - 1];
}

【讨论】:

  • 据我所知,你只是让它变得更复杂了。关键循环具有与以前相同的复杂性,但您添加了另一个可能并行化的循环。导致更多的内存使用和更多的计算。
  • 整个过程(很可能是)内存绑定。即使您对某些并行索引的预计算要通过其并行化来加速(这还有待证明),实际循环中的内存引用数量与以前相同,这将与之前一样昂贵最初的。总而言之,您的方法将比最初的方法花费更多的时间和更多的资源。
  • @KamiKaze,为了节省时间,您必须尽可能地并行化。正如我的回答所见,这种方法以几乎 100% 的速度赢得胜利。当然,我更复杂......但它可以节省时间(大约 50%)
  • @LuisColorado 给定但仅当有一些东西要并行化时,这根本不会改变关键路径,除了可能的 MAC 操作。看看 Gilles 怎么说。
  • 关键路径(如我的回答所示)允许两个并行计算线程,因此您可以节省 50% 的时间。不考虑在几乎所有情况下都可以完全消除外循环。
【解决方案2】:

OUTER LOOP(假定没有别名,允许别名请参见下文)

当您没有引用外部循环控制变量l 并且您甚至没有引用赋值的相同条款并且没有横向效果是在内循环中产生的,运行内循环是幂等的,运行一次或多次没有任何好处,所以一个非常好的优化是完全消除它@987654322 @,如下例所示:

pru.c

00001: #include <stdio.h>
00002: #include <stdlib.h>
00003: #define n 10
00004: #define loop 30


00005: void print(int x[], int y[], int z[])
00006: {
00007:      int i;
00008:      printf("%12s%12s%12s%12s\n","i", "x[]", "y[]", "z[]");
00009:      for(i = 0; i < n; i++)
00010:              printf("%12d%12d%12d%12d\n", i, x[i], y[i], z[i]);
00011: }
00012: int main()
00013: {
00014:      int x[n], y[n], z[n];
00015:      int i, l;
00016:      for(i = 0; i < n; i++) {
00017:              x[i] = rand();
00018:              y[i] = rand();
00019:              z[i] = rand();
00020:      }

打印开头

00021:      print(x, y, z);

接下来是发布的循环:

00022:      for (l = 1; l <= loop; l++) {
00023:              printf("iteration %d\n", l);

00024:         for (i = 1; i<n; i++) {
00025:              x[i] = z[i] * (y[i] - x[i - 1]);
00026:         }

打印出来

00027:              print(x, y, z);

00028:      }

循环结束

00029: }

如您所见,循环传递之间的数组内容没有区别。接下来是运行程序来演示:

初始内容:

$ a.out
           i         x[]         y[]         z[]
           0       33613   564950497  1097816498
           1  1969887315   140734212   940422543
           2   202055087   768218108   770072198
           3  1866991770  1647128879    83392682
           4  1421485336   148486083   229615973
           5   127561358   735081006    33063457
           6  1646757679   287085223  1793088605
           7   802182690   382151770  1848710666
           8  1486775472   115658218   394986197
           9   661076908  1786703631   864107022

第一次迭代:

iteration 1
           i         x[]         y[]         z[]
           0       33613   564950497  1097816498
           1 -1607135687   140734212   940422543
           2  1213242898   768218108   770072198
           3 -1987622590  1647128879    83392682
           4 -1113079323   148486083   229615973
           5  -327431319   735081006    33063457
           6   407021958   287085223  1793088605
           7  1996444744   382151770  1848710666
           8   500660170   115658218   394986197
           9   -84727866  1786703631   864107022
iteration 2
           i         x[]         y[]         z[]
           0       33613   564950497  1097816498
           1 -1607135687   140734212   940422543
           2  1213242898   768218108   770072198
           3 -1987622590  1647128879    83392682
           4 -1113079323   148486083   229615973
           5  -327431319   735081006    33063457
           6   407021958   287085223  1793088605
           7  1996444744   382151770  1848710666
           8   500660170   115658218   394986197
           9   -84727866  1786703631   864107022
iteration 3
           i         x[]         y[]         z[]
           0       33613   564950497  1097816498
           1 -1607135687   140734212   940422543
           2  1213242898   768218108   770072198
           3 -1987622590  1647128879    83392682
           4 -1113079323   148486083   229615973
           5  -327431319   735081006    33063457
           6   407021958   287085223  1793088605
           7  1996444744   382151770  1848710666
           8   500660170   115658218   394986197
           9   -84727866  1786703631   864107022

... 重复迭代直到

iteration 30
           i         x[]         y[]         z[]
           0       33613   564950497  1097816498
           1 -1607135687   140734212   940422543
           2  1213242898   768218108   770072198
           3 -1987622590  1647128879    83392682
           4 -1113079323   148486083   229615973
           5  -327431319   735081006    33063457
           6   407021958   287085223  1793088605
           7  1996444744   382151770  1848710666
           8   500660170   115658218   394986197
           9   -84727866  1786703631   864107022
$ _

内循环

如果你重新排序内部表达式,你也可以在内部循环中获得一些好处,因为

x[0]
 \----.
      |
x[1] <+- y[1], z[1]
  \---.
      |
x[2] <+- y[2], z[2]
  .
  .
  .
x[n-1]<+- y[n-1],z[n-1]
   \--.
      |
x[n] <+- y[n], z[n]

如果将表达式重新排列为x[i] = z[i]*y[i] - z[i]*x[i-1],则可以并行化所有z[i]*y[i]的计算,以及z[i]*x[i-1]的计算,只要x[i-1]的值被计算出来,在计算内循环。

 thrd[0]   thrd[1]    thrd[2]      ... thrd[j]   ...  thrd[n]
============================================================
z[1]*x[0]  z[1]*y[1]     z[2]*y[2] ... z[j]*y[j] ... z[n-1]*y[n-1]
    |          |             |             |                |        
    \----------+-------.     |             |                |
           ,---'       |     |             |                |
           |           |     |             |                |
           V           V     |             |                |
x[1] = z[1]*y[1] - z[1]*x[0] |             |                |
  |                          |             |                |
  `--------------------.     |             |                |
                       |     |             |                |
           ,-----------+-----'             |                |
           |           |                   |                |
           V           V                   |                |
x[2] = z[2]*y[2] - z[2]*x[1]               |                |
  |                                        |                |
  `--------------------.                   |                |
           ,-----------+-------------------'                |
           |           |                                    |
          ...         ...                                   |
           V           V                                    |
x[j] = z[j]*y[j] - z[j]*x[j-1]                              |
...                                                         |                    
 |                                                          |
 `---------------------------.                              |
                             |                              |
               ,-------------+------------------------------'
               |             |
               V             V
x[n-1] = z[n-1]*y[n-1]-z[n-1]*x[n-2]

这可以通过两个线程池有效地计算。以前你有n-1 产品和n-1 减法,现在你有2*n 产品和n-1 减法,并行计算,所以最终你从这种方法中没有节省(你得到两个线程工作,谢谢给向我显示错误的 KamiKaze)

考虑别名

从上图可以看出,内循环的计算只依赖x[0]y[0...n-1]z[0...n-1],而交叉值的唯一依赖由表达式x[1] = f(x[0],z[1],y[1])给出。如果您检查...如果我们将x 别名为zy,则表达式转换为x[j] = f(x[j-1],x[j], y[j])x[j] = f(x[j-1],z[j],x[j]),这使得x[j] 的值通常取决于x[j] 的先前值。在这些情况下(x 别名为yz,或两者兼有)算法不是幂等的,并且无法消除外部循环。在仅将yz 混叠的情况下,表达式为x[j] = f(x[j-1], y[j])(或x[j] = f(x[j-1], z[j])),因此对先前的值不存在依赖性,并且算法是幂等的。

因此,总而言之,如果允许x 向量与yz 中的任何一个之间存在混叠,则无法消除外循环,并且必须保留。如果yz 出现别名,算法继续是幂等的,不需要外循环。

【讨论】:

  • 正如这里所暗示的,如果外循环只是重复内循环计算(不清楚是否如此),编译器可以将外循环缩减为单次迭代,但使用 openmp 并行化这样的可能会阻止优化。
  • @tim18,对!你可能会得到更严重的资源浪费,让几个处理器忙于计算一个幂等的东西。
  • 请不要发布带有行号的代码,这会使尝试您的解决方案变得乏味。
  • 您的代码可能会产生未定义的行为。将整数范围内的两个整数相乘可能会导致值大于 int。
  • 您的内部循环仍然具有与以前相同的关键路径。您必须等待上一次迭代才能进行下一次计算。您可能会得到预先计算的乘法结果,但您仍然必须在关键路径中进行乘法和减法,这不会产生任何好处。例外情况是 MAC 操作(请参阅我的回答)
【解决方案3】:

正如路易斯在他的回答中所说,外循环只是噪音,它一直都在做同样的事情。所以我们不必考虑这个(或者可以将它大量并行化,但这样做一次就足够了)......

内部循环依赖于前一个循环(x[i-1])。

当使用 Multiply-Accumulate 指令时,z * y 将内部循环更改为 x[i] = preComps[i] - z[i] * x[i - 1];(由 dvhh 提出)的预计算可能会产生好处,但这是特定于实现的,我不知道收获。

如果z[] 中有0s,那么就有可能并行化。因为对于z[i] = 0 -> x[i] = 0 让您可以在此时将内部循环拆分为

x[i+1] 将始终等于 y[i+1] * z[i+1],这在任何时候都是已知的。为您提供另一个循环的入口点。

【讨论】:

  • 特别是在原始版本中,像 float *__restrict x 这样声明 x[] 和 y[] 和 z[] 之间没有别名的声明可以通过允许 y 和 z 的负载发生在与先前 x 值的计算并行。如前所述,这里的 openmp 没有明显的价值。
  • @tim18,我必须检查这一点,但内部循环的计算仅取决于先前数组值的值(仅适用于数组 x),而不是 x[0]、@987654332 @ 或 z[0] 被内部循环触及...因此,即使在混叠的情况下,运行内部循环两次也可能对输出没有影响。但是,至少目前,这只是一个假设。我猜只有在数组重叠的情况下,才能有所不同。
  • @LuisColorado 你是对的关于外循环我误判了x[i-1] 的影响。 x[0] 永远不会改变,所以起点是相同的,x 中除 x[0] 之外的所有值都不会被评估。 (重写评论,因为它有一些错误)
猜你喜欢
  • 2017-06-08
  • 2018-12-23
  • 1970-01-01
  • 2014-09-28
  • 1970-01-01
  • 2017-09-01
  • 2020-08-28
  • 1970-01-01
  • 2019-01-02
相关资源
最近更新 更多