为何OpenMP归约在一种场景有效,另一种场景结果不稳定?
第一个代码为啥每次都对?
第一个代码的并行逻辑简单直接,每个线程只处理自己专属的数据块:
- 线程只操作
combined_thread_local_sm_g[tid]和sm_t[tid],这些都是跟线程ID绑定的局部数据,完全不跟其他线程的操作交叉。 - 最后算出来的
num_thread_local_SpEdges[tid]是确定值,只依赖自身处理结果。OpenMP的reduction(+:total_num_sp_edges)会自动给每个线程创建私有副本,最后把所有副本的值合并到全局变量,全程无数据冲突,结果自然稳定正确。
第二个代码为啥结果飘忽不定?
第二个代码的并行逻辑藏着几个坑,导致每次运行的计算过程都有不确定性:
并行循环的判断条件依赖可变共享数据
并行循环里的判断comp_afforest[sortedSNData[i]] != comp_afforest[sortedSNData[i-1]]用到了全局数组comp_afforest和sortedSNData。如果这两个数组在并行执行过程中被其他线程修改(比如代码未展示的其他并行逻辑),每个线程的判断结果会随机变化,local_splitters的元素个数每次都不一样,最终归约出来的total_num_splitters结果必然不一致。归约变量未完成合并就被提前访问
在#pragma omp single块里直接调用splitters.resize(total_num_splitters),此时的total_num_splitters只是当前执行single块的线程的私有副本(因parallel区域声明了reduction),并非所有线程计算完成后的合并值。虽然这不会直接搞乱total_num_splitters本身,但后续基于这个不完整值的操作可能引发连锁问题,进一步加剧结果的随机性。线程初始化与并行调度的耦合有隐患
线程0先往local_splitters中插入了初始值0,并行循环用static调度把i的范围分给不同线程。如果comp_afforest或sortedSNData存在线程间可见性问题(比如未正确同步),线程0的初始值和其他线程的计算结果会出现混乱组合,导致最终统计的分裂点数量不稳定。
一句话总结
第一个代码的并行操作全是线程独立处理局部数据,无共享数据冲突,归约逻辑安全可控;第二个代码依赖可能被修改的共享数据,还提前访问了未合并的归约变量,计算过程充满不确定性,结果自然每次都不一样。
内容的提问来源于stack exchange,提问作者Faysal

