【问题标题】:How does OpenMP use the atomic instruction inside reduction clause?OpenMP 如何在归约子句中使用原子指令?
【发布时间】:2021-04-01 00:37:26
【问题描述】:

OpenMP 如何在归约构造函数中使用atomic 指令? 完全不依赖原子指令吗?

例如,下面代码中的变量sum是用atomic'+'运算符累加的吗?

#include <omp.h>
#include <vector>

using namespace std;
int main()
{
  int m = 1000000; 
  vector<int> v(m);
  for (int i = 0; i < m; i++)
    v[i] = i;

  int sum = 0;
  #pragma omp parallel for reduction(+:sum)
  for (int i = 0; i < m; i++)
    sum += v[i];
}

【问题讨论】:

    标签: c++ c multithreading parallel-processing openmp


    【解决方案1】:

    OpenMP 如何在归约内使用原子指令?不是吗 完全依赖原子?

    由于 OpenMP 标准没有指定应该(或不)如何实现 reduction 子句(例如, 基于 atomic 操作与否),它的实现可能会有所不同,具体取决于每个OpenMP 标准的具体实现。

    例如,下面代码中的变量 sum 是用 原子+运算符?

    不过,根据 OpenMP 标准,我们可以阅读以下内容:

    归约子句可用于执行某些形式的递归 并行计算(...)。对于并行和工作共享结构,一个 创建每个列表项的私有副本,为每个隐式任务创建一个, 好像使用了私人条款一样。 (...) 私人副本是 然后按上面指定的方式进行初始化。 在区域的末尾 指定减少子句的,原始列表项是 通过将其原始值与每个的最终值相结合来更新 私有副本的数量,使用指定的组合器 减少标识符。

    因此,基于此,可以推断归约子句中使用的变量将是私有的,因此不会自动更新。尽管如此,即使不是这种情况,OpenMP 标准的具体实现也不太可能依赖于atomic 操作(对于指令sum += v[i];),因为(在这种情况下)不会是最有效的策略。有关为什么会出现这种情况的更多信息,请查看以下 SO 线程:

    1. Why my parallel code using openMP atomic takes a longer time than serial code?;
    2. Why should I use a reduction rather than an atomic variable?

    非常非正式地,比使用atomic 更有效的方法是让每个线程拥有自己的变量sum 的副本,并且在parallel region 的末尾,每个线程将其副本保存到一个线程间共享资源——现在,根据减少的实现方式,atomic 操作可能用于更新该共享资源。然后该资源将被 master 线程拾取,该线程将减少其内容并相应地更新原始 sum 变量。

    更正式的来自OpenMP Reductions Under the Hood

    在详细回顾了并行归约之后,您可能仍然 对 OpenMP 如何真正改变您的 顺序代码转换为并行代码。特别是,您可能想知道 OpenMP 如何检测循环体中执行的部分 减少。例如,这个或类似的代码片段可以 经常在代码示例中找到:

     #pragma omp parallel for reduction(+:x)
     for (int i = 0; i < n; i++)
         x -= some_value;
    

    您也可以使用 - 作为归约运算符(实际上是 冗余到+)。但是 OpenMP 如何隔离 更新步骤 x-= some_value?令人不安的答案是 OpenMP 根本没有检测到更新!编译器处理 像这样的for循环:

    #pragma omp parallel for reduction(+:x)
         for (int i = 0; i < n; i++)
             x = some_expression_involving_x_or_not(x);
    

    因此,对 x 的修改也可以隐藏在不透明的 > 函数调用后面。 从编译器的角度来看,这是一个可以理解的决定 开发商。不幸的是,这意味着您必须确保所有 x 的更新与定义的操作兼容 减少子句。

    归约的整体执行流程可以概括为 如下:

    1. 产生一组线程并确定每个线程 j 必须执行的迭代集。
    2. 每个线程都声明归约变量 x 的私有化变体,该变体用相应的中性元素 e 初始化 幺半群。
    3. 所有线程都执行它们的迭代,无论它们是否或如何涉及私有化变量的更新。
    4. 结果计算为对(局部)部分结果和全局变量 x 的顺序归约。最后,结果是 写回 x。

    【讨论】:

      【解决方案2】:

      检查生成的程序集有时很有用。例如,GCC 为我生成了以下指令 (live demo):

        ...
        add rcx, rax
        xor eax, eax
        lea rcx, [rsi+4+rcx*4]
      .L4:
        add eax, DWORD PTR [rdx]
        add rdx, 4
        cmp rdx, rcx
        jne .L4
        mov ecx, eax
      .L3:
        lock add DWORD PTR [rbx+12], ecx
        ...
      

      这些指令由并行区域内的所有线程执行。 .L4: 标签表示由实际线程执行的循环部分。线程局部和的结果累加到eaxadd eax, DWORD PTR [rdx])中,它首先被清零(xor eax, eax)。最后,将结果移动到ecx 并从ecx 移动到代表sum 变量的内存位置。

      请注意,由于add 指令的lock 前缀,此内存位置的增量是原子的

      使用 GCC,因此您的问题的答案是肯定的——它确实使用原子增量,但每个线程最后只执行一次。 (理论上,可以在每次迭代中使用共享结果的原子增量,但这会非常低效。即使禁用了优化,GCC 也不会这样做。)

      【讨论】:

      • GCC 在每次迭代中不增加共享结果并禁用优化的原因不是它在理论和实践上都非常低效,而是因为这不符合 reduction 的语义OpenMP 规范中概述的子句。归约变量是私有的。
      【解决方案3】:

      reduction 子句中可能隐藏着原子,但可能不是您期望的。

      首先,sum += v[i]; 不是使用原子操作实现的,因为它不需要。 OpenMP 规范非常清楚,归约变量只不过是具有以下附加语义的私有变量:

      • 与常规私有变量不同,归约变量被初始化为归约运算符的零值;对于+,该值为0
      • 归约变量的各个私有值在归约绑定到的 OpenMP 构造末尾合并(归约)为原始变量的值。

      第二部分原子操作可能被实现使用。正如@dreamcrash 所解释的,OpenMP 规范没有规定如何(甚至何时)准确地减少单个值。不管怎样,reduction 所做的就相当于改造

      int sum = 0;
      
      #pragma omp parallel for reduction(+:sum)
      for (int i = 0; i < m; i++)
        sum += v[i];
      

      进入

      int sum = 0;
      
      #pragma omp parallel
      {
        int sum_priv = 0;
      
        #pragma omp for
        for (int i = 0; i < m; i++)
          sum_priv += a[i];
      
        // BEGIN actual reduction
        #pragma omp atomic update
        sum += sum_priv;
        // END actual reduction
      }
      

      表示的部分是实际减少发生的地方,即sum 变量将使用各个私有副本的值进行更新。它可以使用atomic 来实现,如图所示,但还有更有效的方法,例如成对树缩减,甚至可以是函数调用。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-04-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-07-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多