【问题标题】:OpenMP parallel-for efficiency queryOpenMP 并行效率查询
【发布时间】:2017-05-10 16:36:16
【问题描述】:

请考虑使用以下简单代码对 parallel for 循环中的值求和:

int nMaxThreads = omp_get_max_threads();
int nTotalSum = 0;
#pragma omp parallel for num_threads(nMaxThreads) \
    reduction(+:nTotalSum)
    for (int i = 0; i < 4; i++)
    {
        nTotalSum += i;
        cout << omp_get_thread_num() << ": nTotalSum is " << nTotalSum << endl;
    }

当我在双核机器上运行它时,我得到的输出是

0: nTotalSum is 0
0: nTotalSum is 1
1: nTotalSum is 2
1: nTotalSum is 5

这向我表明关键部分,即nTotalSum 的更新,正在每个循环中执行。这似乎是一种浪费,因为每个线程所要做的就是计算它添加的值的“本地”总和,然后在它完成后用这个“本地总和”更新nTotalSum

我对输出的解释是否正确,如果正确,我怎样才能使其更有效率?注意我尝试了以下方法:

#pragma omp parallel for num_threads(nMaxThreads) \
    reduction(+:nTotalSum)
    int nLocalSum = 0;
    for (int i = 0; i < 4; i++)
    {
        nLocalSum += i;
    }
    nTotalSum += nLocalSum;

...但是编译器抱怨说它期望在pragma omp parallel for 语句之后出现for 循环...

【问题讨论】:

    标签: c++ openmp


    【解决方案1】:

    您的输出实际上并不表示循环期间的关键部分。每个线程都有自己的零初始化副本,线程 0 在 i = 0,1 上工作,线程 1 在 i = 2,3 上工作。最后,OpenMP 负责将本地副本添加到原始副本。

    除非您有具体证据表明您可以更有效地实施,否则您不应该尝试自己实施。参见例如this question / answer

    如果您将 parallel / for 拆分为两个指令,您的手动版本将起作用:

    int nTotalSum = 0;
    #pragma omp parallel
    {
      // Declare the local variable it here!
      // Then it's private implicitly and properly initialized
      int localSum = 0;
      #pragma omp for
      for (int i = 0; i < 4; i++) {
        localSum += i;
        cout << omp_get_thread_num() << ": nTotalSum is " << nTotalSum << endl;
      }
      // Do not forget the atomic, or it would be a race condition!
      // Alternative would be a critical, but that's less efficient
      #pragma omp atomic
      nTotalSum += localSum;
    }
    

    我认为您的 OpenMP 实现很可能就是这样做的。

    【讨论】:

    • 谢谢。我通过在循环之前和循环内输出其地址来确认每个线程都有自己的nTotalSum 本地副本。正如预期的那样,有三个不同的地址;循环之前(和之后)的变量,以及两个线程中每个线程中的变量。
    【解决方案2】:

    每个 OMP 线程都有自己的 nTotalSum 副本。在 OMP 部分的末尾,这些被组合回原来的 nTotalSum。您看到的输出来自在一个线程中运行循环迭代 (0,1),在另一个线程中运行 (2,3)。如果您在循环结束时输出nTotalSum,您应该会看到预期的结果 6。

    在您的nLocalSum 示例中,将nLocalSum 的声明移动到#pragma omp 行之前。 for 循环必须紧跟在 pragma 之后。

    【讨论】:

      【解决方案3】:

      来自我在 openmp 书中的并行编程:

      reduction 子句可能更难理解,具有私有和共享存储行为。归约属性用于作为算术归约目标的对象。这在许多应用程序中可能很重要...减少允许编译器有效地实现它...这是一个如此常见的操作,openmp 具有减少数据范围子句来处理它们...最常见的示例是最终求和并行构造结束时的临时局部变量。

      更正你的第二个例子:

      total_sum = 0;  /* do all variable initialization prior to omp pragma */
      
      #pragma omp parallel for \
                  private(i) \
                  reduction(+:total_sum)
      
         for (int i = 0; i < 4; i++)
         {
             total_sum += i;  /* you used nLocalSum here */
         }
      
      #pragma omp end parallel for
      
      /* at this point in the code,
         all threads will have done your `for` loop where total_sum is local to each thread,
         openmp will then '+" together the values in `total_sum` coming from each thread because we used reduction,
         do not do an explicit nTotalSum += nLocalSum after the omp for loop, it's not needed the reduction clause takes care of this
      */
      

      在您的第一个示例中,我不确定您使用 #pragma omp parallel for num_threads(nMaxThreads) reduction(+:nTotalSum)num_threads(nMaxThreads) 正在做什么。但我怀疑奇怪的输出可能是由打印缓冲引起的。

      无论如何,如果使用得当,reduction 子句非常有用且非常有效。在更复杂的真实示例中会更加明显。

      您发布的示例非常简单,以至于它没有展示归约子句的有用性,严格来说,对于您的示例,因为所有线程都以最有效的方式进行求和这样做只会使total_sum 成为并行部分中的共享变量,并让所有线程进入它。最后答案仍然是正确的。 如果使用关键指令会起作用

      【讨论】:

      • “最有效的方法就是让 total_sum 成为并行部分中的共享变量,并让所有线程进入它。”这既是错误的(竞争条件)又是低效的(争用)。您的代码看起来不像 C/OpenMP。
      • 抱歉,提交错误。我把它打掉了。试图表明是否不存在复杂的数据依赖关系,除了减少子句之外还有其他方法。而且我经常将 fortran 语法与 c 混淆,我更改了更改后的 pragma 语法。
      • 对于 2 级循环,使用带有 simd 归约的内部求和和使用并行归约的外部求和可能更有效。详细信息因编译器而异。
      猜你喜欢
      • 2012-06-27
      • 2016-01-22
      • 1970-01-01
      • 2021-10-24
      • 1970-01-01
      • 2012-11-01
      • 2012-02-09
      • 1970-01-01
      相关资源
      最近更新 更多