OpenMP加速Fortran90子程序失效:单线程反而更快的原因排查
并行化最近点搜索变慢的原因及优化方案
核心问题分析
你的OpenMP并行版本比单线程慢,本质是并行开销远大于计算收益,再加上代码本身存在几个设计缺陷,具体如下:
循环调度与负载不均衡
你用了OpenMP默认的static调度拆分外层I_C循环,但如果I_COL_START到I_COL_STOP的范围不大,每个线程分到的任务量极少。线程创建、上下文切换的固定开销,直接盖过了并行计算节省的时间,16线程时这种开销会被进一步放大。内层循环频繁调用线程ID
在最内层doLIN循环里每次都调用OMP_GET_THREAD_NUM(),这个函数调用有固定开销,内层循环迭代次数多的话,累积成本非常高。线程ID在并行区域内是固定的,完全可以只获取一次。反复内存分配/释放
每次调用子程序都要分配、初始化、释放三个数组,这部分额外开销在并行场景下会被放大。如果这个子程序被频繁调用,这部分成本会变得很显著。内存访问模式不友好
Fortran默认是列优先存储,你的代码先循环列索引I_C,再循环行索引I_L,导致访问ST_REF_GRID%RT2_x(I_C,I_L)时是跳跃式的内存访问,缓存命中率极低。并行后多个线程同时竞争缓存,进一步降低了内存访问效率。
优化后的代码示例
针对以上问题,调整后的代码如下:
SUBROUTINE COLOC_LIST_2_REFGRID_OMP(I_T,I_NTASK,I_LIN_START,I_LIN_STOP,I_COL_START,I_COL_STOP,& R_MIN_DIS,I_COL_INDEX,I_LIN_INDEX) IMPLICIT NONE INTEGER :: I_T,I_NT,I_NTASK,I_thread_id,I_LIN_START,I_LIN_STOP,I_COL_START,I_COL_STOP,I_C,I_L INTEGER,ALLOCATABLE,DIMENSION(:),SAVE :: IT1_COL_MIN_INDEX,IT1_LIN_MIN_INDEX REAL, PARAMETER :: PR_MAX_DIS=999.0 REAL :: R_DIS REAL,ALLOCATABLE,DIMENSION(:),SAVE :: RT1_MIN_DIS !OUTPUT INTEGER :: I_COL_INDEX,I_LIN_INDEX REAL :: R_MIN_DIS !---------------------------------------------------------------------------------------------- ! 仅在首次调用时分配内存,避免反复分配释放 IF (.NOT.ALLOCATED(RT1_MIN_DIS)) THEN ALLOCATE(IT1_COL_MIN_INDEX(0:I_NTASK-1)) ALLOCATE(IT1_LIN_MIN_INDEX(0:I_NTASK-1)) ALLOCATE(RT1_MIN_DIS(0:I_NTASK-1)) ENDIF ! 初始化线程私有最小值 DO I_NT=0,I_NTASK-1 IT1_COL_MIN_INDEX(I_NT)=0 IT1_LIN_MIN_INDEX(I_NT)=0 RT1_MIN_DIS(I_NT)=PR_MAX_DIS ENDDO !$omp parallel & !$omp shared (I_COL_START,I_COL_STOP,I_LIN_START,I_LIN_STOP,IT1_COL_MIN_INDEX,IT1_LIN_MIN_INDEX,RT1_MIN_DIS) & !$omp private (I_C,I_L,I_thread_id,R_DIS) ! 只在并行区域开头获取一次线程ID I_thread_id = OMP_GET_THREAD_NUM() !$omp do schedule(dynamic) ! 用dynamic调度适配负载,也可根据情况用guided doLIN: DO I_L=I_LIN_START,I_LIN_STOP ! 调整循环顺序:先循环行,适配列优先存储 doCOLON: DO I_C=I_COL_START,I_COL_STOP ! 先比较平方值减少计算量,最后再取sqrt R_DIS=(ST_OBS%RT1_x(I_T)-ST_REF_GRID%RT2_x(I_C,I_L))**2 + & (ST_OBS%RT1_y(I_T)-ST_REF_GRID%RT2_y(I_C,I_L))**2 IF (R_DIS.lt.RT1_MIN_DIS(I_thread_id)) THEN RT1_MIN_DIS(I_thread_id)=R_DIS IT1_LIN_MIN_INDEX(I_thread_id)=I_L IT1_COL_MIN_INDEX(I_thread_id)=I_C ENDIF ENDDO doCOLON ENDDO doLIN !$omp end do !$omp end parallel ! 全局找最小值,最后计算sqrt R_MIN_DIS=PR_MAX_DIS DO I_NT=0,I_NTASK-1 IF (RT1_MIN_DIS(I_NT).lt.R_MIN_DIS) THEN R_MIN_DIS=RT1_MIN_DIS(I_NT) I_COL_INDEX=IT1_COL_MIN_INDEX(I_NT) I_LIN_INDEX=IT1_LIN_MIN_INDEX(I_NT) ENDIF ENDDO R_MIN_DIS=SQRT(R_MIN_DIS) ENDSUBROUTINE COLOC_LIST_2_REFGRID_OMP
额外优化点
- 去掉不必要的sqrt:比较距离时直接用平方值,最后找到最小值再计算平方根,减少浮点运算量。
- 选择合适的调度策略:如果循环迭代次数不均,
schedule(dynamic)或schedule(guided)比默认的static更能平衡负载。 - 避免频繁调用子程序:如果这个子程序被大量调用,可以把并行逻辑放到外层循环,减少线程创建/销毁的开销。
内容的提问来源于stack exchange,提问作者ManuF
相关产品推荐
相关产品推荐

