作者介绍
谢依晖
湖南大学硕士研究生在读,
本科毕业于湖南大学计算机科学与技术专业
本文调研了4篇与OpenMP优化相关的文献,对优化点分析如下:
面向Open64的OpenMP程序优化[1]
跨越过程边界的并行区重构
Open64有着过程间分析优化部件,因此可以知道哪些函数使用了被调函数,从而可以通过在使用被调函数处放置合适的编译指导语句来完成并行区重构。
这样做的好处是:
- 进一步扩大并行块的大小;
- 将并行块提升到调用函数中,便于进一步对调用函数中的并行块合并。
以下给出例子:
`
program main
call sub_procedure
endsubroutine sub_procedure
!$omp parallel
P
!$omp end parallel
end
`优化后:
`program main
!$omp parallel
call sub_procedure
!$omp end parallel
endsubroutine sub_procedure
P
end
`
OpenMP并行编程模型与性能优化方法的研究及应用[2]
Cache命中率优化
- 数组合并:定义两个数组val[N]和key[N],在顺序访问val[i]和key[i]时可能会导致Cache冲突失效,若改为struct merge{key, val}就可以通过提高空间局部性减少Cache失效次数。
- 循环交换:C按行存储而Fortran按列存储,应根据存储的顺序来访问。
- 提取关键数据:提取关键数据可以减少重复存取的数据,例如在排序中用关键字和指针代替整个记录排序,这样就能让Cache无需存放无关数据而提高命中率。
- 分块:对于极大大小的数组,要在Cache中一次容纳整个数组是有困难的,但可以将数组分为多块,可有效降低Cache失效率。
循环调度优化
在OpenMP中可对并行循环指定调度方案,以将每个迭代分配给多个工作线程执行。其一般形式如下:
#pragma omp for schedule(schedule_name, chunk_size) for(i = 0; i < N; i++)
OpenMP编译与优化技术研究[3]
论文中给出了一种使用启发式规则来估计各种额外开销和调度参数的关系,得到一个线性不等式组,可以通过求解该不等式组得到较优的调度参数。
变量属性的优化
在OpenMP语句中每一次对变量的声明都对应一次新的地址分配。给出以下例子:
#pragma omp parallel { #pragma omp for private(a) {...} #pragma omp for private(a) {...} }
在如上代码中,编译器会为每个循环分配一个单独的私有变量,而优化后的代码如下所示:
#pragma omp parallel private(a) { #pragma omp for {...} #pragma omp for {...} }
How to Get Good Performance by Using OpenMP[4]
去除依赖
对于某些循环语句,存在依赖而导致无法使用OpenMP优化,但是这其中的某些依赖可以通过修改代码去除依赖而使用OpenMP运行代码。
下列循环存在反依赖:
for(int i = 0; i < n; i++) { x = (b[i] + c[i]) / 2; a[i] = a[i + 1] + x; }
除去循环之间的依赖后:
#pragma omp parallel for shared(a, a_copy) for(int i = 0; i < n; i++) { a_copy[i] = a[i + 1]; } #pragma omp parallel for shared(a, a_copy) private(x) for(int i = 0; i < n; i++) { x = (b[i] + c[i]) / 2; a[i] = a_copy[i] + x; }
下列循环存在流依赖:
for(int i = 1; i < n; i++) { b[i] = b[i] + a[i - 1]; a[i] = a[i] + c[i]; }
在loop skewing之后:
b[1] = b[1] + a[0] #pragma omp parallel for shared(a, b, c) for(int i = 1; i < n - 1; i++) { a[i] = a[i] + c[i]; b[i + 1] = b[i + 1] + a[i]; } a[n - 1] = a[n - 1] + c[n - 1];
负载不均衡
下段代码使用流水线形式处理,以块的形式读取数据,然后处理每个块并在下一个块之前将结果写入磁盘,造成极差的负载均衡。
for(i = 0; i < N; i++) { readfromfile(i, ...); for(int j = 0; j < processingnum; j++) { processdata(); //lots of work } writetofile(i); }
接下来这段代码使用动态调度来重叠I/O和处理数据,将上述流水线代码并行化。
`#pragma omp parallel
{
/* preload data to be used in first iteration of the i-loop /
#pragma omp single
{ReadFromFile(O,...);}
for (i=0; i<N; i++) {
/ preload data for next iteration of the i-loop /
#pragma omp single nowait
{ReadFromFile(i+1...);}
#pragma omp for schedule(dynamic)
for (j=0; j<ProcessingNum; j++)
ProcessChunkOfData(); / here is the work /
/ there is a barrier at the end of this loop /
#pragma omp single nowait
{writeResultsToFile(i);}
} / threads immediately move on to next iteration of i-loop /
} / one parallel region encloses all the work */`
解决伪共享问题
int a[Nthreads][cache_line_size]; #pragma omp parallel for shared(Nthreads, a) schedule(static,1) for (int i = 0; i < Nthreads; i++) a[i] += i;
一般情况下,int型变量占四个字节,A[0]和A[1]的地址只差四个字节,小于一个Cache行,它们有着极大的可能在同一Cache行内,从而导致同时更新不同处理器的相同Cache行中的单个元素会导致整个Cache行无效。
对于False sharing问题,一般可以通过填充数组来优化。
int a[Nthreads][cache_line_size]; #pragma omp parallel for shared(Nthreads, a) schedule(static,1) for (int i = 0; i < Nthreads; i++) a[i][0] += i;
我们还对文献中的部分优化使用LLVM Flang编译器和classic-flang编译器进行了测试,测试结果请参考https://gitee.com/src-openeuler/flang/pulls/22/files。
References
- 刘京,郑启龙,李彭勇,郭连伟.面向Open64的OpenMP程序优化[J].计算机系统应用,2016,25(01):154-159.
- 游佐勇. OpenMP并行编程模型与性能优化方法的研究及应用[D].成都理工大学,2011.
- 陈永健. OpenMP编译与优化技术研究[D].清华大学,2004.
- http://akira.ruc.dk/~keld/teaching/IPDC_f10/Slides/pdf4x/4_Performance.4x.pdf