控制并行循环中的线程数并减少开销
Controlling Number of Threads in Parallel Loops & Reducing Overhead
在我的 Fort运行95 代码中,我有一系列嵌套的 DO 循环,整个循环需要大量时间来计算,所以我想用 OpenMP 添加并行功能(使用 gfortran -fopenmp
到 compile/build).
有一个主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
但是,当我将它设置为 运行 时,我没有从 运行ning 并行代码中获得我期望的加速。我预计前几个会因开销而变慢,但过了一段时间后,我预计并行代码 运行 比顺序代码快,但事实并非如此。我比较了主 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
我的第一个想法是我没有正确设置线程数。
问题:
- 我构建并行代码的方式是否有明显错误?
- 是否有更好的方法来实现我已经完成/想做的事情?
- 没什么显然错了,但是如果串行循环花费很长时间,您的加速将受到限制。进行并行计算可能需要重新设计您的算法。
- 不要设置循环中的线程数,而是使用
!$omp master
- !$omp end master
指令将执行减少到单个线程。添加一个 !$omp barrier
如果你可以 运行 只有在所有其他线程完成后这个块。
确实存在明显错误的地方:您已从代码中删除了所有并行性。在创建最外层的并行区域之前,您将其大小定义为一个线程。因此,只会创建一个线程来处理该区域内的任何代码。随后使用 omp_set_num_threads(4)
不会改变这一点。这个调用只是说下一个 parallel
指令将创建 4 个线程(除非另有明确要求)。但是没有这样的新 parallel
指令,它会在当前指令中 nested 。您只有一个工作共享 do
指令应用于一个唯一线程的当前封闭 parallel
区域。
有两种方法可以解决您的问题:
保持代码原样:虽然形式上,您将在进入和退出 parallel
区域时分叉和加入线程,但 OpenMP 标准不要求线程被创建和销毁。实际上,它甚至鼓励线程保持活动状态以减少 parallel
指令的开销,这是由大多数 OpenMP 运行-time 库完成的。因此,问题的这种简单方法的有效载荷并不算太大。
使用第二种方法将 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 是永远的。
在我的 Fort运行95 代码中,我有一系列嵌套的 DO 循环,整个循环需要大量时间来计算,所以我想用 OpenMP 添加并行功能(使用 gfortran -fopenmp
到 compile/build).
有一个主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
但是,当我将它设置为 运行 时,我没有从 运行ning 并行代码中获得我期望的加速。我预计前几个会因开销而变慢,但过了一段时间后,我预计并行代码 运行 比顺序代码快,但事实并非如此。我比较了主 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
我的第一个想法是我没有正确设置线程数。
问题:
- 我构建并行代码的方式是否有明显错误?
- 是否有更好的方法来实现我已经完成/想做的事情?
- 没什么显然错了,但是如果串行循环花费很长时间,您的加速将受到限制。进行并行计算可能需要重新设计您的算法。
- 不要设置循环中的线程数,而是使用
!$omp master
-!$omp end master
指令将执行减少到单个线程。添加一个!$omp barrier
如果你可以 运行 只有在所有其他线程完成后这个块。
确实存在明显错误的地方:您已从代码中删除了所有并行性。在创建最外层的并行区域之前,您将其大小定义为一个线程。因此,只会创建一个线程来处理该区域内的任何代码。随后使用 omp_set_num_threads(4)
不会改变这一点。这个调用只是说下一个 parallel
指令将创建 4 个线程(除非另有明确要求)。但是没有这样的新 parallel
指令,它会在当前指令中 nested 。您只有一个工作共享 do
指令应用于一个唯一线程的当前封闭 parallel
区域。
有两种方法可以解决您的问题:
保持代码原样:虽然形式上,您将在进入和退出
parallel
区域时分叉和加入线程,但 OpenMP 标准不要求线程被创建和销毁。实际上,它甚至鼓励线程保持活动状态以减少parallel
指令的开销,这是由大多数 OpenMP 运行-time 库完成的。因此,问题的这种简单方法的有效载荷并不算太大。使用第二种方法将
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 是永远的。