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

Posted

技术标签:

【中文标题】控制并行循环中的线程数并减少开销【英文标题】:Controlling Number of Threads in Parallel Loops & Reducing Overhead 【发布时间】:2017-05-07 12:47:01 【问题描述】:

在我的 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

一些嵌套的 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

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

在此之后,我认为打开和关闭主循环任一侧的并行代码是有意义的(因此只应用一次开销),并将线程数设置为 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

但是,当我将其设置为运行时,我并没有获得运行并行代码所期望的加速。我希望前几个会慢一些来解决开销,但过了一段时间我希望并行代码比顺序代码运行得更快,但事实并非如此。对于DO a = 1, 50,我比较了主 DO 循环每次迭代的运行速度,结果如下:

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

我的第一个想法是我没有正确设置线程数。

问题:

    我构建并行代码的方式是否有明显问题? 有没有更好的方法来实现我已经完成/想要做的事情?

【问题讨论】:

您已将并行设置始终设置为 1 个线程。 你能具体说明一下我是在哪里做的吗? 【参考方案1】:

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

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

    保持您的代码原样:尽管形式上,您将在进入和退出 parallel 区域时分叉并加入您的线程,但 OpenMP 标准不要求创建和销毁线程。实际上,它甚至鼓励线程保持活动状态以减少 parallel 指令的开销,这是由大多数 OpenMP 运行时库完成的。因此,这种简单的问题处理方法的payload并不会太大。

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

最后一个版本如下所示:

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

现在这个版本是否真的会比天真的版本更快,由你来测试。

最后一句话:由于您的代码中有很多连续的部分,所以不要期望太多的加速。 Amdahl's law 是永远的。

【讨论】:

好吧,这是有道理的。我的印象是我可以更改下一个“OMP 语句”的线程数,但显然这仅指 !$OMP PARALLEL 而不仅仅是 !$OMP DO。现在很明显,我创建了一个只有一个工作线程的并行区域,谢谢!【参考方案2】:
    显然没有错,但如果串行循环需要很长时间,您的加速将受到限制。进行并行计算可能需要重新设计算法。 不要设置循环中的线程数,而是使用!$omp master - !$omp end master 指令将执行减少到单个线程。如果您只能在所有其他线程完成后运行此块,请添加 !$omp barrier

【讨论】:

所以添加 !$OMP MASTER // !$OMP END MASTER 代码的任一侧我只想运行一次?如果我这样做并删除第一个 set_num_threads(1),那么它会为每个线程启动主 DO 循环,然后崩溃。

以上是关于控制并行循环中的线程数并减少开销的主要内容,如果未能解决你的问题,请参考以下文章

Android线程池(转)

Android中的线程池

深入线程池

详解Go语言调度循环源码实现

java线程池相关知识点总结

线程池的优点?