OMP SECTIONS 和 DO 在同一个 PARALLEL 块中
Posted
技术标签:
【中文标题】OMP SECTIONS 和 DO 在同一个 PARALLEL 块中【英文标题】:OMP SECTIONS and DO in the same PARALLEL block 【发布时间】:2016-10-23 07:34:11 【问题描述】:我有 3 个任务完全相互独立,因此非常适合并行执行:
任务 1:执行名为 subA() 的(单线程)子例程。
任务 2:执行名为 subB() 的(单线程)子例程。
任务 3:在 DO 循环中填充数组。 DO 循环的每次迭代都独立于所有其他迭代。
假设我有 8 个线程。我希望线程 0 处理任务 1,线程 1 处理任务 2,线程 2-7 处理任务 3。在 Fortran 中,我想像这样:
COMPLEX*8, EXTERNAL :: func
!$OMP PARALLEL
!$OMP SECTIONS
!$OMP SECTION
!
! Task 1, performed by one thread
!
CALL subA()
!$OMP SECTION
!
! Task 2, performed by one thread
!
CALL subB()
!$OMP END SECTIONS NOWAIT
!$OMP DO
!
! Task 3, performed by all threads
!
DO j=1,nn
vals(j) = func(j)
END DO
!$OMP END DO NOWAIT
!$OMP END PARALLEL
但是上面的代码并不是我想要的。在任务 1 和 2 上工作的线程也被安排在任务 3 中的 DO 循环上工作,这似乎减慢了一切,大概是因为这 2 个线程“迟到”到达 DO 循环,因此所有其他线程必须在 PARALLEL 区域末端的隐式屏障处等待它们。
在这种情况下处理线程调度的正确方法是什么?
冒着提供太多信息的风险,我已经知道 subA() 和 subB() 是计算密集型的,而 func(j) 的每次评估都比较快。完成 subA() 和 subB() 所需的时间与完成后一个任务的多个线程时完成整个 DO 循环所需的时间大致相同。
一些注意事项:
-
将 NUM_THREADS(N) 子句添加到“OMP PARALLEL”并将 NUM_THREADS(N-2) 子句添加到“OMP DO”是很诱人的。但我认为“OMP DO”构造不接受 NUM_THREADS 子句。
一种解决方案是在 DO 循环上使用 DYNAMIC 调度。这样,并行 DO 循环将在执行 subA() 和 subB() 完成后拾取线程。这可行,但并不令人满意,因为 DYNAMIC 调度增加了不小的开销,因此增加了 DO 循环的执行时间,令人不快。
此处出现相关问题:put multiple do-s and section-s in the same parallel environment。但是,该问题的答案仅表明可以将 SECTIONS 和 DO 与相同的 PARALLEL 环境结合使用,但并未解决我在此处提出的调度问题。
我最初在英特尔的开发者论坛here 上提出了这个问题,并建议我交叉发布到 ***。
ETA:英特尔的某个人指出我最初的问题是模棱两可的:不清楚我的 DO 循环是否只是从 func(1..nn) 到 vals(1..nn) 的 memcpy() 还是我正在调用一个名为 func() 的函数 nn 次。后者是我的意图,我在示例代码中对此进行了说明。
【问题讨论】:
正如我在 Ryan 的原始帖子中已经提到的,在 OMP DO 中添加 schedule 子句似乎过于频繁,无法防止在我的不同情况下进入矢量化分支。这可以通过任意使其成为嵌套的 simd 内部并行外部循环来避免。这个问题可能被认为是“开销”,但在我的情况下,它不是 OpenMP 开销,而是未能使用 simd 移动指令,而是命中未优化的剩余循环。在 Ryan 的情况下,可能会有自动 memcpy 替换。 【参考方案1】:使用 OpenMP 任务,类似这样(未经测试)
!$omp parallel
! Have a single thread create all the tasks.
!$omp single
!$omp task
call subA()
!$omp end task
!$omp task
call subB()
!$omp end task
DO j=1,nn
!$omp task
vals(j) = func(j)
!$omp end task
END DO
!$omp end single
!$omp end parallel
【讨论】:
我怀疑如果 nn 相对较小,这种方法会很好地工作。但就我而言,nn 是中等的(数十万个),创建数十万个 OpenMP 任务会大大降低速度。 如果您有一个 OpenMP 4.5 编译器,您可以使用 taskloop 构造将内部循环替换为其显式任务创建。然后您可以在任务循环上指定粒度。或者(如果你只有一个较旧的编译器)你可以自己强制它(在块上有一个外部循环来创建任务,然后执行一个内部循环),它仍然会比你在下面提出的解决方案更小,更便携。 (当然,除非您想在具有 72 个 coresx4threads 的 KNL 上运行时编写 288 个 switch case :-))。【参考方案2】:到目前为止,我发现的最佳解决方案是手动执行我希望 OpenMP 自动为我执行的调度。特别是,让 nthr 为我希望使用的线程数。然后我使用 SELECT CASE 构造如下:
SELECT CASE (nthr)
CASE (3)
! Use 3 threads. See example below
CASE (4)
! Use 4 threads. See example below
CASE (5)
! And so on...
CASE DEFAULT
! Catch-all
END SELECT
如果我有 4 个线程,我会执行以下操作:
!$OMP PARALLEL PRIVATE(j) NUM_THREADS(4)
!$OMP SECTIONS
!$OMP SECTION
!
! Task 1, performed by one thread
!
CALL subA()
!$OMP SECTION
!
! Task 2, performed by one thread
!
CALL subB()
!$OMP SECTION
!
! One thread does half the loop...
!
DO j=1,nn/2
vals(j)=func(j)
END DO
!$OMP SECTION
!
! ...and one thread does the other half
!
DO j=nn/2+1,nn
vals(j)=func(j)
END DO
!$OMP END SECTIONS NOWAIT
!$OMP END PARALLEL
nthr 的其他值的情况是对上述代码的明显修改。
从正确和快速的角度来看,此解决方案运行良好,但从软件工程的角度来看,代码重复使其不是最理想的。
【讨论】:
以上是关于OMP SECTIONS 和 DO 在同一个 PARALLEL 块中的主要内容,如果未能解决你的问题,请参考以下文章
OpenMP:不能同时使用 omp parallel for 和 omp task 吗? /错误:工作共享区域可能没有紧密嵌套在工作共享内
通过分离#omp parallel 和#omp for 来减少 OpenMP fork/join 开销