作者介绍
谢依晖
湖南大学硕士研究生在读,
本科毕业于湖南大学计算机科学与技术专业
?
Abstract
本文调研了一些对OpenMP优化的方式:
Matthias Müller开发了一个Benchmark,并用手写优化后代码和优化前代码对多款编译器进行测试是否已经支持文章提到的几种优化方式[1]。
在早期的OpenMP设计中,编译器前端产生的不少优化障碍是无法通过常用的编译器中端优化技术来克服的,如阻止了常量传播等各种编译器经典转换,这些优化障碍严重影响了性能。Johannes,Hal等在现有的LLVM/Clang编译器工具链上提出了一些优化方法,缓解了这些优化障碍[2]。
Some Simple OpenMP Optimization Techniques
可选代码
在有些时候,使用OpenMP对程序进行并行化的性能不如串行运行。因此可以在较小的工作负载时避免并行执行来减少额外开销。手写代码的解决方案如下:
?
if?(condtion)?then ?!$omp?parallel ??!?code ?!$omp?end?parallel else ?!?code end?if
?
孤立指令
孤立指令如果没有动态地包含在并行区域中,OpenMP 标准规定“被视为遇到一个大小为1的线程组”。线程为1的并行化通常会有更差的性能,尽管手写版本要多调用一个函数,可它依然有着更好的性能。
手写优化版本:
?
if?(omp_in_parallel())?then ?!$omp?parallel ??!?code ?!$omp?end?parallel else ?!?code end?if
?
合并并行区域
在循环没有依赖关系时,连接上下两个循环:
?
!$omp?parallel?do ?do?i?=?1,?100 ??a(i)?=?i ?end?do !$omp?end?parallel?do !$omp?parallel?do ?do?i?=?1,?100 ??b(i)?=?i ?end?do !$omp?end?parallel?do
?
在并行区域末尾添加隐式的nowait
因为在循环和并行区域的末端之间没有代码,所以不需要多个barrier。因此可以增加nowait消除多余的barrier。
?
do?n?=?1,?100000 ?!$omp?parallel ??!$omp?do ??do?i?=?1,?100 ???a(i)?=?i ??end?do ??!$omp?end?do?nowait ?!$omp?end?parallel end?do
?
通过OpenMP指令帮助优化代码
?
do?i?=?1,?100 ?a(index(i))?=?a(index(i))?+?b(i) end?do
?
这个代码,因为index[i]对编译器未知,编译器不能假设循环之间是独立的。但是加上?!$omp parallel do?后,如果这个循环可以并行执行,那么这个代码同样也可以用software pipelining 或者 vectorization来优化。
Compiler Optimizations for OpenMP
属性传播
程序员可以在代码中使用例如const或者是restrict属性,这能够让程序员更好地传递执行轨迹集信息给编译器以便后续的优化。同样,编译器也可以采用属性说明通过分析而得到一些信息。
笔者创建了一个LLVM传播通道,它在并行工作函数的参数声明中传递以下属性:
缺少指针捕获
访问行为(只读,只写)
缺少可被访问者调用的别名指针
指针的对齐,非空和 dereferencability 信息
在此简单给一个例子,源代码如下:
?
int?foo()?{ ?int?a?=?0; ?#pragma?omp?parallel?shared(a) ?{ ??#pragma?omp?critical ??{?a?+=?1;?} ??bar(); ??#pragma?omp?critical ??{?a?*=?2;?} ?} ?return?a; }
?
以下代码为编译器前端为源代码产生的伪C风格表示:
?
int?foo()?{ ?int?a?=?0; ?int?*restrict?p?=?&a; ?omp_parallel(pwork,?p); ?return?a; } void?pwork(int?tid,?int?*p)?{ ?if?(omp_critical_start(tid))?{ ??*p?=?*p?+?1; ??omp_critical_end(tid); ?} ?bar(); ?if?(omp_critical_start(tid))?{ ??*p?=?*p?*?2; ??omp_critical_end(tid); ?} }
?
优化后的代码:
?
void?pwork(int?tid,?int?*restrict?p)?{ ?if?(omp_critical_start(tid))?{ ??*p?+=?1; ??omp_critical_end(tid); ?} ?bar()[p];?//?May?"use"?pointer?p. ?if?(omp_critical_start(tid))?{ ??*p?*=?2; ??omp_critical_end(tid); ?} }
?
变量私有化
OpenMP代码涉及对所有变量的区域外声明和区域内使用的冗长、易错的分类。笔者根据变量的实际使用情况对变量分类进行转换:
Shared:任何修改都可能对其它线程可见,也能在并行域之后可见。
Firstprivate:一个私有变量,但是使用并行域之前的值进行初始化。
Private:变量的本地线程的未初始化副本,类似于并行域中的shadowing重声明。
从shared、firstprivate到private,允许对串行部分和并行部分使用单独的变量,从而对两个部分都做额外的优化。但是如果下面的条件都满足,那么私有化是允许的:
并行域结束后,在它的下一次使用之前,(重新)赋值过;
并行域内每个变量使用之前,都在并行域内赋值过;
变量的使用和它使用前的最后一次赋值没有潜在的barrier。
此外,还可以用值传递代替引用传递,如果他们是live-in且不是live-out以及不用于线程间通信,这将是合理的。如果上面的条件只有第一个和最后一个满足,将会传递变量的值。
最后,非live-out的变量可能可以在并行域前私有化,如果第一个条件成立,就用串行代码中声明的新变量的值替换并行域中的值。
并行域扩张
根据硬件的不同,并行域的开始和结束由于fork-join模式可能会增加大量的成本。以下代码作为例子:
?
while?(ptr?!=?end)?{ ?#pragma?omp?parallel?for?firstprivate(ptr) ?for?(int?i?=?ptr->lb;?i?ub;?i++) ??forward_work(ptr,?i); ?#pragma?omp?parallel?for?firstprivate(ptr,?a) ?for?(int?i?=?ptr->ub;?i?>?ptr->lb;?i--) ??backward_work(ptr,?a,?i?-?1); ?ptr?=?ptr->next; }
?
外部循环和两个并行域之间不存在依赖,为了降低fook和join的成本并改进程序内分析,扩展了相邻的并行程序:
?
while?(ptr?!=?end)?{ ?#pragma?omp?parallel?firstprivate(ptr,?a) ?{ ??#pragma?omp?for?firstprivate(ptr)?nowait ??for?(int?i?=?ptr->lb;?i?ub;?i++) ???forward_work(ptr,?i); ??#pragma?omp?barrier?//?explicit?loop?end?barrier ??#pragma?omp?for?firstprivate(ptr,?a)?nowait ??for?(int?i?=?ptr->ub;?i?>?ptr->lb;?i--) ???backward_work(ptr,?a,?i?-?1); ??#pragma?omp?barrier?//?explicit?loop?end?barrier ?} ?ptr?=?ptr->next; }
?
为了进一步减少开销,扩展并行域也可以对串行构造展开,这只有在串行结构能得到适应的保护以及不会干扰并行语义的情况下进行。不过需要注意的是,以下优化代码会增加一个新的barrier:
?
#pragma?omp?parallel?shared(ptr)?firstprivate(a) { ?while?(ptr?!=?end)?{ ??#pragma?omp?for?firstprivate(ptr)?nowait ??for?(int?i?=?ptr->lb;?i?ub;?i++) ???forward_work(ptr,?i); ??#pragma?omp?barrier?//?explicit?loop?end?barrier ??#pragma?omp?for?firstprivate(ptr,?a)?nowait ??for?(int?i?=?ptr->ub;?i?>?ptr->lb;?i--) ???backward_work(ptr,?a,?i?-?1); ??#pragma?omp?barrier?//?explicit?loop?end?barrier ??#pragma?omp?master ??{?ptr?=?ptr->next;?} ??#pragma?omp?barrier?//?barrier?for?the?guarded?access ?} }
?
通信优化
串行代码和并行代码部分之间的运行时库间接性不仅禁止信息传输,也禁止代码运动。运行时函数调用的参数是在串行部分和并行部分之间通信的变量。这些变量是由前端根据代码位置和捕获语义确定的。
笔者提出的方法将执行常量传播,按值而不是按引用来传递参数,尽量减少要传递的变量的数量,将变量提出循环和并行区域。对优化前的如下代码,希望在通信时K和M被提出循环,N被512替代。
优化前:
?
void?f(int?*X,?int?*restrict?Y)?{ ?int?N?=?512;???//movable ?int?L?=?*X;?????//immovable ?int?A?=?N?+?L;??//movable ?#pragma?omp?parallel?for?firstprivate(X,?Y,?N,?L,?A) ?for?(int?i?=?0;?i??
优化后:
?
void?f(int?*X,?int?*restrict?Y)?{ ?int?L?=?*X; ?int?K?=?*Y; ?int?M?=?512?*?K; ?#pragma?omp?parallel?firstprivate(X,?M,?L) ?{ ??int?A?=?512?+?L; ??#pragma?omp?for?firstprivate(X,?M,?A,?L) ??for(int?i?=?0;?i?512;?i++) ???X[i]?=?M?+?A?*?L?*?i; ?} }?
?
审核编辑:汤梓红
评论