使用指针执行OpenMP归约性能远逊普通变量的优化咨询
我需要在C语言中使用单元素指针变量执行OpenMP归约操作,目前已知两种实现方式:
double *ptr = ... #pragma omp for reduction(max:ptr[0]) // 或者 #pragma omp for reduction(max:ptr[:1])
但使用指针执行归约的性能显著低于普通变量。以下为测试示例:
int REP = 1000000, ITER = 1000; double red_var() { double foo = 0.0; #pragma omp parallel for (int i = 0; i < REP; ++i) { #pragma omp for reduction(+:foo) for (int j = 0; j < ITER; ++j) { foo += 1.0 / (j + 1); } } return foo; } double red_ptr() { double foos[1]; double *foo = foos; #pragma omp parallel for (int i = 0; i < REP; ++i) { #pragma omp for reduction(+:foo[0]) for (int j = 0; j < ITER; ++j) { *foo += 1.0 / (j + 1); } } return *foo; }
在我的系统中,red_var()函数的速度几乎是red_ptr()的3倍。
系统环境
- CPU: Intel Xeon Gold 5120
- OS: Ubuntu Server 22.04
- 编译器: GCC v11.3.0
- 编译选项:
-O3 -fopenmp
分析生成的汇编代码发现,编译器在普通变量归约中使用lock cmpxchg指令,而指针归约则使用OpenMP原子区域。请问是否有方法让编译器在使用指针时也采用lock cmpxchg指令?
补充信息
lock cmpxchg并非在内部for循环的每次迭代中执行,编译器会使用私有累加器,仅在循环结束时执行归约到共享变量的操作。- 创建
const restrict指针告知编译器无别名且指针不会更新,并未改变生成的汇编代码。 - 将归约从
omp for移至omp parallel可提升性能,但仅因为归约仅执行一次,编译器仍为指针归约创建原子区域。 - 用C++编译并将指针替换为引用可解决该问题,引用版本生成的汇编代码使用
lock cmpxchg。 - 添加
-ffast-math或-march=native编译标志似乎未改善生成的汇编代码或性能。 - Clang(v14.0.0)和ICX(v2023.1.0)编译器在两种归约中均使用
lock cmpxchg,生成的二进制文件两者执行时间相近。
解决方案
这是GCC 11.x版本对OpenMP指针归约的优化局限性导致的问题,针对当前环境有几种可行的处理方式:
升级GCC版本
GCC 12及后续版本优化了指针式归约的处理逻辑,会自动采用lock cmpxchg替代原子区域,升级到最新稳定版后大概率能解决性能差异问题。改用C++编译并替换为引用
你已经验证过这种方式有效——C引用的语义更接近普通变量,GCC对引用归约的优化逻辑和普通变量一致,会直接生成高效的lock cmpxchg归约代码,若项目允许用C编译,这是最直接的解决办法。直接使用数组元素替代指针
若必须用C语言,可以直接用数组元素做归约(比如直接写foos[0]而非foo[0]),部分场景下GCC会对直接数组访问生成更优的归约代码:double red_arr() { double foo[1] = {0.0}; #pragma omp parallel for (int i = 0; i < REP; ++i) { #pragma omp for reduction(+:foo[0]) for (int j = 0; j < ITER; ++j) { foo[0] += 1.0 / (j + 1); } } return foo[0]; }切换到Clang/ICX编译器
你已经验证过这两款编译器在两种归约方式下都能生成lock cmpxchg代码,性能差异可以忽略,若项目对编译器没有强依赖,切换到这两款编译器也是可行方案。
本质上,GCC 11.x对指针归约的保守处理是因为指针的别名分析复杂度更高,编译器无法确定指针指向的内存是否存在其他别名引用,所以选择了更安全的原子区域。即使添加restrict关键字,该版本的OpenMP优化模块也没能正确识别这个属性,因此优化策略没有变化。
内容的提问来源于stack exchange,提问作者LorienLV

