含数据依赖的循环代码如何用OpenMP优化?求技术指导
问题描述
我已许久未使用OpenMP,在优化以下代码时遇到困难:
#define SIZE 100000000 typedef struct { float a,b,c,d,e,f,g,h; } s_t; void compute(s_t *a) { int i; for (i=4; i<SIZE; i++) { a[i].a=(a[i-1].b * 0.42 + a[i-3].d * .32); a[i].b = a[i].c * 3.56 - a[i-3].f; a[i].c = a[i].d + a[i].g*a[i-3].d; a[i].d = .3334/sqrt(a[i].f*a[i].f + a[i].c*a[i].c); if (a[i].f + a[i].a>1e-3) a[i].f = (a[i].f - a[i].a)*(a[i].f + a[i].a); } } int main() { int i; s_t *a; a=(s_t *)malloc(sizeof(s_t)*SIZE); /* Initialization */ for (i=0; i<SIZE; i++) a[i].a=a[i].b=a[i].c=a[i].d=a[i].e=a[i].f=a[i].g=a[i].h=1./(i+1); /* Computation */ for(i=0;i<100;i++) { compute(a); fprintf(stderr,"."); } fprintf(stderr,"%f ",a[10].a); free(a); return 0; }
我希望在compute函数的循环上使用#pragma omp parallel for,但该循环存在多处数据依赖。我尝试过使用depend子句,但认为让a[i]依赖a[i-1]和a[i-3]会导致代码串行执行。我不清楚如何用OpenMP处理该问题,能否提供相关思路或指导?此外,若有其他用OpenMP或其他方法优化该代码的建议,也请告知。
解决方案与优化思路
一、处理循环数据依赖:波前并行(流水线并行)
你的循环存在真依赖(写后读):a[i].a依赖a[i-1].b,a[i].b/c/d/f部分依赖更早的a[i-3]成员。直接用parallel for会引发数据竞争,单纯按i依赖i-1的depend子句也会退化为串行,但可以通过波前并行挖掘并行性:
- 用OpenMP任务实现依赖驱动的并行:
每个迭代i只需等待i-1和i-3的迭代完成即可启动,不同区间的满足依赖条件的迭代可以并行执行(比如i=7依赖i=6和i=4,完成后i=7可与i=8、i=9等同时执行)。代码示例:
注意:需要编译器支持OpenMP 4.0及以上版本,且SIZE足够大(1亿)时,任务调度开销可被摊薄,并行效率会更明显。void compute(s_t *a) { #pragma omp parallel #pragma omp single { for (int i=4; i<SIZE; i++) { #pragma omp task depend(in: a[i-1].b, a[i-3].d, a[i-3].f) depend(out: a[i].a, a[i].b, a[i].c, a[i].d, a[i].f) { a[i].a=(a[i-1].b * 0.42 + a[i-3].d * .32); a[i].b = a[i].c * 3.56 - a[i-3].f; a[i].c = a[i].d + a[i].g*a[i-3].d; a[i].d = .3334/sqrt(a[i].f*a[i].f + a[i].c*a[i].c); float sum_fa = a[i].f + a[i].a; if (sum_fa > 1e-3) a[i].f = (a[i].f - a[i].a) * sum_fa; } } } }
二、其他优化建议
1. 内存访问优化
- 结构体成员重排:把频繁访问的成员(a、b、c、d、f)放在结构体最前面,减少缓存行浪费,提升缓存局部性。
- AoS转SoA:将结构体的每个成员单独做成数组(如
float *a_arr, *b_arr, *c_arr...),连续访问同一成员时缓存命中率更高,也更利于SIMD指令的利用。
2. SIMD向量化优化
- 开启编译器自动向量化选项:比如GCC/Clang的
-O3 -mavx2,让编译器尝试优化无依赖的计算片段。 - 替换冗余计算:用
hypotf(a[i].f, a[i].c)替代sqrt(a[i].f*a[i].f + a[i].c*a[i].c),标准库函数的向量化支持更完善。
3. 减少迭代内冗余
- 提前计算重复使用的值:比如
a[i].f + a[i].a在判断和计算中都用到,可提前存储为临时变量,避免重复计算。
4. 循环展开
- 手动展开循环(如一次处理2-4个迭代),减少循环控制开销,同时帮助编译器生成更优的机器码。需注意保持依赖逻辑的正确性。
5. 多轮迭代优化
- 主函数中
compute被调用100次,可尝试将这100次迭代与内部循环合并,或利用数据局部性减少重复内存加载的开销(需确保逻辑正确)。
内容的提问来源于stack exchange,提问作者Unemployed Goose
相关产品推荐
相关产品推荐

