C中的OpenMP共享数组:还原与原子更新问题

问题描述 投票:0回答:1

我无法确定此OpenMP任务示例出了什么问题。就上下文而言,y是一个大型共享数组,并且rowIndex对于每个任务都不是唯一的。可能有多个任务试图增加值y [rowIndex]。

我的问题是,y是否需要由归约子句保护,还是原子更新就足够了?我目前正遇到一个更大的程序崩溃,并且想知道我是否正在对此进行基本设置。

我看到的示例中,由于每个线程的数组复制,大多数数组减少都是针对非常小的数组,而大多数原子更新未在数组元素上使用。似乎没有太多内容可以一次在一个竞标条件下更新一个元素的共享数组(而且基于任务的并行性的情况很少见。)

#pragma omp parallel shared(y) // ??? reduction(+:y) ???
#pragma omp single
for(i = 0; i < n; i++)
{  
  sum = DoSmallWork_SingleThread();  
  rowIndex = getRowIndex_SingleThread();

  #pragma omp task firstprivate(sum, rowIndex)  
  {    
    sum += DoLotsOfWork_TaskThread();

    // ??? #pragma omp atomic update ???
    y[ rowIndex ] += sum;  
  }
}
arrays task openmp atomic reduction
1个回答
2
投票

您基本上有3种解决方案来避免这类竞争条件,您都提到了。它们都工作得很清楚:

  1. atomic access,即让线程/任务在同一时刻访问同一数组,但确保正确的操作顺序,这是通过对带有shared子句的数组使用atomic子句来完成的在操作上:

    #pragma omp parallel
    #pragma omp single
    for(i = 0; i < n; i++)
    {
        sum = DoSmallWork_SingleThread();
        rowIndex = getRowIndex_SingleThread();
    
        #pragma omp task firstprivate(sum, rowIndex) shared(y)
        {
            increment = sum + DoLotsOfWork_TaskThread();
    
            #pragma omp atomic
            y[rowIndex] += increment;
        }
    }
    
  2. 私有化,即每个任务/线程都有其自己的数组副本,然后将它们汇总在一起,这是reduction子句的作用:

    #pragma omp parallel
    #pragma omp single
    #pragma omp taskgroup task_reduction (+:y[0:n-1])
    for(int i = 0; i < n; i++)
    {
        int sum = DoSmallWork_SingleThread();
        int rowIndex = getRowIndex_SingleThread();
    
        #pragma omp task firstprivate(sum, rowIndex) in_reduction(+:y[0:n-1])
        {
            y[rowIndex] += sum + DoLotsOfWork_TaskThread();
        }
    }
    
  3. [[[exclusive access对数组或数组部分的访问,这是用于任务相关性的(例如,您可以为基于线程的并行性模型使用互斥锁来实现):]]

    #pragma omp parallel
    #pragma omp single
    for(i = 0; i < n; i++)
    {
        sum = DoSmallWork_SingleThread();
        rowIndex = getRowIndex_SingleThread();
    
        #pragma omp task firstprivate(sum, rowIndex) depend(inout:y[rowIndex])
        {
            y[rowIndex] += sum + DoLotsOfWork_TaskThread();
        }
    }
    
  4. 什么时候应该使用它们?

  1. 原子访问是一种较慢的内存访问类型,可提供一致性保证,在发生冲突(即两个(或多个)线程尝试同时修改同一值的情况下,尤其慢)。

    [仅当对y的更新很少且相距很远并且发生冲突的可能性较低时,才优选使用原子。

  2. 私有化通过制作阵列的副本并将它们连接在一起(在您的情况下,将它们添加在一起)来避免冲突问题。

    y的大小成比例,这会导致内存开销,并可能影响高速缓存。

  3. 最后,提供任务依赖性通过使用scheduling

    完全避免了该问题,即仅同时运行修改数组各个部分的任务。通常,当y较大并且修改y的操作在任务中很频繁时,这是首选。

    但是,您的并行性受到您定义的依赖项数量的限制,因此在上面的示例中,受到y中的行数的限制。例如,如果您只有8行但有32个内核,那么这可能不是最佳方法,因为您只会使用25%的计算能力。

  4. NB:这意味着在私有化(aka简化)的情况下,尤其是在依赖关系的情况下,您可以通过将数组y的各个部分组合在一起而受益,通常是让任务在多个连续的行上进行操作。然后,您可以减少(分别减少,或者增加依赖关系的大小)任务子句中提供的数组块的大小。

© www.soinside.com 2019 - 2024. All rights reserved.