控制并行循环中的线程数并减少开销

con*_*l_c 5 parallel-processing fortran loops openmp gfortran

在我的Fortran 95代码中,我有一系列嵌套的DO循环,整个过程需要大量的时间来计算,所以我想用OpenMP添加并行功能(gfortran -fopenmp用于编译/构建).

有一个主要的DO循环,运行1000次.

其中有一个子DO循环,运行100次.

其他几个DO循环嵌套在此中,迭代次数随着DO循环的每次迭代而增加(第一次一次,最后一次最多1000次).

例:

DO a = 1, 1000

    DO b = 1, 100

        DO c = 1, d
            some calculations
        END DO

        DO c = 1, d
            some calculations
        END DO

        DO c = 1, d
            some calculations
        END DO
    END DO
    d = d + 1
END DO
Run Code Online (Sandbox Code Playgroud)

一些嵌套的DO循环必须以串行方式运行,因为它们本身包含依赖关系(也就是说,循环的每次迭代都有一个包含上一次迭代的值的计算),并且在这种情况下不能轻易并行化.

我可以轻松地使没有任何依赖项的循环并行运行,如下所示:

d = 1
DO a = 1, 1000

    DO b = 1, 100

        DO c = 1, d
            some calculations with dependencies
        END DO
!$OMP PARALLEL
!$OMP DO
        DO c = 1, d
            some calculations without dependencies
        END DO
!$OMP END DO
!$OMP END PARALLEL
        DO c = 1, d
            some calculations with dependencies
        END DO
    END DO
    d = d + 1
END DO
Run Code Online (Sandbox Code Playgroud)

但是我知道打开和关闭并行线程会有很大的开销,因为这在循环中会发生很多次.代码运行速度明显慢于以前按顺序运行时的速度.

在此之后,我认为在主循环的任一侧打开和关闭并行代码是有意义的(因此只应用一次开销),并将线程数设置为1或8以控制部分是按顺序运行还是并行,如下:

d = 1
CALL omp_set_num_threads(1)
!$OMP PARALLEL
DO a = 1, 1000

    DO b = 1, 100

        DO c = 1, d
            some calculations with dependencies
        END DO
    CALL omp_set_num_threads(4)
!$OMP DO
        DO c = 1, d
            some calculations without dependencies
        END DO
!$OMP END DO
    CALL omp_set_num_threads(1)

        DO c = 1, d
            some calculations with dependencies
        END DO
    END DO
    d = d + 1
END DO
!$OMP END PARALLEL
Run Code Online (Sandbox Code Playgroud)

但是,当我设置它运行时,我没有得到运行并行代码所期望的加速.我希望前几个开销能够较慢,但是过了一段时间我希望并行代码比顺序代码运行得更快,但事实并非如此.我比较了主DO循环的每次迭代的运行速度DO a = 1, 50,结果如下:

Iteration    Serial    Parallel
1            3.8125    4.0781              
2            5.5781    5.9843              
3            7.4375    7.9218              
4            9.2656    9.7500              
...                              
48           89.0625   94.9531                
49           91.0937   97.3281                
50           92.6406   99.6093
Run Code Online (Sandbox Code Playgroud)

我的第一个想法是我不知何故没有正确设置线程数.

问题:

  1. 我如何构建并行代码有明显的错误吗?
  2. 有没有更好的方法来实现我已经/想要做的事情?

Gil*_*les 2

确实存在一些明显错误的地方:您已从代码中删除了所有并行性。在创建最外层并行区域之前,您将其大小定义为一个线程的大小。因此,只会创建一个线程来处理该区域内的任何代码。随后使用omp_set_num_threads(4)不会改变这一点。这个调用仅仅表示无论下一个parallel指令将创建 4 个线程(除非明确要求)。但没有这样的新parallel指令,它本来可以嵌套在当前指令中。您只有一个do应用于parallel一个唯一线程的当前封闭区域的工作共享指令。

有两种方法可以解决您的问题:

  1. 保持代码不变:尽管形式上,您将在进入和退出该parallel区域时分叉和加入线程,但 OpenMP 标准并不要求创建和销毁线程。实际上,它甚至鼓励线程保持活动状态以减少指令的开销parallel,大多数 OpenMP 运行时库都这样做。因此,这种简单方法解决问题的有效负载并不算太大。

  2. 使用第二种方法将parallel指令推到最外层循环之外,但创建工作共享所需的尽可能多的线程(我相信这里有 4 个线程)。parallel然后,您可以使用指令将您所在区域内必须连续的内容括起来single。这将确保不会发生与额外线程的不需要的交互(隐式屏障和退出时刷新共享变量),同时避免您不想要的并行性。

最后一个版本看起来像这样:

d = 1
!$omp parallel num_threads( 4 ) private( a, b, c ) firstprivate( d )
do a = 1, 1000
    do b = 1, 100
!$omp single
        do c = 1, d
            some calculations with dependencies
        end do
!$omp end single
!$omp do
        do c = 1, d
            some calculations without dependencies
        end do
!$omp end do
!$omp single    
        do c = 1, d
            some calculations with dependencies
        end do
!$omp end single
    end do
    d = d + 1
end do
!$omp end parallel
Run Code Online (Sandbox Code Playgroud)

现在这个版本是否真的比原始版本更快,这取决于你的测试。

最后一点是:由于代码中有相当多的连续部分,因此无论如何都不要期望有太多的加速。阿姆达尔定律是永恒的。