OpenMP+Fortran矩阵行归约求和的最优实现方案咨询
我有一个规模为N×T的大型矩阵DATA(T为线程数),需要对矩阵的每一行元素求和,得到一个长度为N的输出向量SUM。串行实现代码如下:
DO j=1,N DO i=1,T SUM(j) = SUM(j) + DATA(j, i) END DO END DO
请问在OpenMP中最快的实现方式是什么?我了解可以在内部循环使用PARALLEL-DO-REDUCTION,编译器会采用二叉树算法对每行元素求和,每行需log(T)步,但N次独立的二叉树归约会带来大量开销。
是否存在更高效的方式?我希望运行单次二叉树归约算法,每一步对DATA的整列进行求和(而非单行元素),仍保持log(T)步,但每步完成N次加法,且并行执行。是否有现成的OpenMP构造可实现该需求?还是需要手动编码?
编辑说明
经过讨论后调整了循环边界顺序,以下是完整Fortran程序。实际应用中N和T约为(6000,40),且在外层循环中运行10000次,但该规模运行仅需约1毫秒,无法进行性能分析,因此代码中调整为(6000000,40)。另外编辑器对N/T的宏定义语句存在显示问题,故补充如下:
#define N 6000000 #define T 40
完整程序代码:
PROGRAM AddArray INTEGER :: i, j REAL, DIMENSION (:,:), POINTER :: p_DATA REAL, DIMENSION (:), POINTER :: p_sum INTEGER :: cr, cm, s1, s2, count_rate, count_max call system_clock(count_rate=cr) call system_clock(count_max=cm) ALLOCATE (p_DATA(N, T)) ALLOCATE (p_sum(N)) !! Initialize data DO i=1,T p_sum(j) = 0.0 DO j=1,N p_DATA(j, i) = j END DO END DO !! Add Reduction on rows of DATA[] call system_clock(s1) DO i=1,T DO j=1,N p_sum(j) = p_sum(j) + p_DATA(j, i) END DO END DO call system_clock(s2) WRITE(0,*) "TIMING: Loop 2: ", (s2-s1) * 1 DEALLOCATE (p_DATA) DEALLOCATE (p_sum) END
1. 纠正初始思路误区
你担心的“N次独立二叉树归约开销大”在T=40的场景下几乎可以忽略——log₂(40)仅约5步,且编译器的reduction优化已经非常成熟,不会产生额外的大量开销。反而你设想的“整列二叉树归约”,内存访问模式和行求和的匹配度更低,容易触发缓存不命中,实际性能未必更好。
2. 最优OpenMP实现(基于现有循环结构)
针对N远大于T的场景(比如6000000 vs 40),最直接高效的方式是并行化外层行循环(j循环),同时对每行的T个元素做归约。每个线程负责一批行的求和,由于T很小,每行的元素能轻松被缓存容纳,内存访问效率很高,编译器会自动优化归约逻辑。
修改后的核心代码:
!! 并行化行循环+归约 call system_clock(s1) !$OMP PARALLEL DO DEFAULT(NONE) PRIVATE(i) SHARED(DATA, SUM, N, T) REDUCTION(+:SUM) DO j=1,N DO i=1,T SUM(j) = SUM(j) + DATA(j, i) END DO END DO !$OMP END PARALLEL DO call system_clock(s2)
3. 贴合内存布局的更优方案(列优先遍历)
Fortran是列优先存储,DATA(:,i)是连续内存块,因此可以反过来遍历列,并行化列循环直接累加整列到SUM向量——这种方式内存访问完全连续,缓存命中率极高,在N很大时性能会更突出:
!! 直接初始化SUM为0,比循环赋值高效 p_sum = 0.0 !! 并行化列循环,累加整列 call system_clock(s1) !$OMP PARALLEL DO DEFAULT(NONE) PRIVATE(j) SHARED(p_DATA, p_sum, N, T) DO i=1,T DO j=1,N p_sum(j) = p_sum(j) + p_DATA(j, i) END DO END DO !$OMP END PARALLEL DO call system_clock(s2)
这种方式不需要显式reduction,编译器会自动处理线程间的竞争:要么通过原子操作,要么给每个线程分配私有副本最后合并,性能表现稳定。
4. 关于“单次二叉树归约整列”的可行性
OpenMP没有现成构造直接实现这种整列级的二叉树归约,手动编码反而会增加复杂度,且在T=40的场景下,收益远不如优化内存访问模式。如果一定要尝试,思路是:
- 先将T个列分组,并行计算每组内的列和到临时数组
- 递归合并临时数组直到得到最终SUM向量
但这种实现代码繁琐,且在T较小时性能提升可以忽略,甚至不如直接并行化列循环。
5. 额外优化建议
- 初始化SUM向量直接用
p_sum = 0.0,比循环赋值效率高很多 - 编译时开启O3优化+OpenMP选项(如
gfortran -O3 -fopenmp),编译器会做更多底层优化 - 你的测试代码初始化部分有bug:
DO i=1,T里的p_sum(j) = 0.0中j未初始化,应移到循环外改为p_sum = 0.0
内容的提问来源于stack exchange,提问作者JB_User

