本节摘要:单节点内有几十个核,OpenMP 是榨取它们的标准工具。本节讲清三件事:并行区与并行循环怎么标记,私有变量与归约的规则(并行最容易翻车的两处),以及数据竞争如何保护。最后落到 DO CONCURRENT 与自动向量化——把并行意图写进语言,让编译器替你干活。
机器有八个核,代码却在用一个。常见原因不是"没开并行",而是编译器不知道循环能不能并行。OpenMP 的价值就在这:你用一行注释式的指令告诉编译器"这个循环可以并行",它负责分配线程、切分迭代、汇总结果。相比手动 pthread 或 MPI,OpenMP 是渐进式的——老代码加几行指令就能跑满多核,不动算法结构。
基本模式是三个指令的循环:!$omp parallel do 标记并行循环,!$omp end parallel do 结束,中间夹归约子句。编译时加 -fopenmp(gfortran)。
program omp_demo use iso_fortran_env, only: real64 implicit none integer, parameter :: n = 1000000 real(real64) :: a(n), s integer :: i a = [(real(i, real64), i = 1, n)] s = 0.0_real64 !$omp parallel do reduction(+:s) private(i) do i = 1, n s = s + a(i) * a(i) ! 每个线程算自己的部分,最后归约求和 end do !$omp end parallel do write(*,*) 'sum of squares =', s end program omp_demo
reduction(+:s) 是关键:每个线程维护 s 的私有副本,循环结束再归约。没有它,多个线程同时写 s 就是数据竞争,结果随机。
并行循环的坑集中在变量归属。循环体里出现的每个变量都要问:它是共享的(各线程共用同一份)还是私有的(各线程各一份)?默认规则:循环索引自动私有,其余变量共享。所以循环体内"临时变量没声明私有"是头号 bug 源——两个线程同时写同一个临时变量,互相踩。
subroutine omp_correct(a, b, n) use iso_fortran_env, only: real64 implicit none real(real64), intent(in) :: a(n) real(real64), intent(out) :: b(n) integer, intent(in) :: n real(real64) :: tmp ! 循环内临时变量 integer :: i !$omp parallel do private(i, tmp) schedule(static) do i = 1, n tmp = a(i) * 2.0_real64 ! tmp 必须私有,否则线程互相踩 b(i) = tmp + 1.0_real64 end do !$omp end parallel do end subroutine omp_correct
归约之外的第二种翻车:循环体内调用"有副作用"的过程——比如每次都写同一个全局计数器的子程序。解决办法:把这样的过程改成纯过程(第 4 章的 pure),或把累加移出循环。OpenMP 对 pure 过程在并行区里可以放心调用,这正是第 4 章强调 pure 属性的回报。
多个线程读同一变量没问题,写同一变量必须保护。两个基本工具:critical 指令保证一段代码同时只有一个线程执行(慢,别放热路径);atomic 指令针对单条原子更新(快)。
program atomic_demo use iso_fortran_env, only: real64 implicit none real(real64) :: total integer :: i total = 0.0_real64 !$omp parallel do do i = 1, 1000 !$omp atomic total = total + 0.5_real64 ! 原子更新,避免竞争 end do !$omp end parallel do write(*,*) total end program atomic_demo
critical 适合多条语句的互斥区,atomic 适合单条更新。更优的思路是"设计上避免竞争":把共享累加改造成归约,把共享状态改成线程私有。保护手段是兜底,消除共享才是治本。
Fortran 2008 的 DO CONCURRENT 不走 OpenMP,它在语言层面声明"迭代之间无数据依赖"。编译器据此自由并行与向量化。约束是:迭代体不能访问非纯过程、不能有 transfer 类语句。
program dc_demo use iso_fortran_env, only: real64 implicit none real(real64) :: u(1000) integer :: i do concurrent (i = 1:1000) u(i) = sin(real(i, real64) * 0.01_real64) end do write(*,*) sum(u) end program dc_demo
DO CONCURRENT 的优势是语义纯净、无线程概念、可移植性好,配合 OpenMP 的 !$omp do concurrent 还能映射到 GPU。它不能直接写共享累加(得靠归约数组再加总),但"迭代独立"的承诺本身就是优化许可证。适合用它替换"本来就独立、只是写了普通循环"的代码。
线程之上的另一层是 SIMD 向量化:一条指令处理多个数据。编译器自动向量化的几个前提:内存访问连续(回忆第 3 章的 contiguous)、循环无依赖、无函数调用(或调用可内联的 pure 过程)、数据对齐。写代码时保持数组连续、用整体数组操作或 DO CONCURRENT、避免在热循环里放指针间接访问,就是在给向量化铺路。gfortran 用 -O3 -march=native 开启,-fopt-info-vec 能打印向量化报告,对照着看哪段循环没被向量化。
阅读完本节,你应当能够:
-fopenmp 编译-fopt-info-vec 报告多核榨干了,下一个战场是加速器——GPU 异构计算。