转载自:http://www.lingcc.com/2009/11/18/10000/
这是Fred chow 在德拉华大学所讲的open64课程讲稿的翻译。若需要原文ppt,请发邮件向我索取。
转载请注明出处: http://lingcc.com
Fred Chow 原版幻灯片见最后一页
1,历史:
1980-83 斯坦福大学RISC编译器研究
1987 MIPS Ucode编译器(R2000) -O2下的全局优化
1991 MIPS UCode编译器(R4000) -O3下的循环优化
另外:
1989 Cydrome Cydra5编译器 软流水优化
1994 SGI Ragnarok编译器(R8000) 浮点性能优化(Floating-pt performance?)
1997年SGI将上面两个分支连同斯坦福SUIF的工作,Rice的IPA整合在一起发布MIPSpro编译器(R10000)
2000年Pro64/Open64编译器(安腾)诞生
2,Open64大事记:
1994:MIPS R10000编译器开发工作开始
1996 :SGI MIPSpro 编译器合并
1998:开始移植后端到安腾,并将前段变更为gcc/g++
2000: Pro64编译器通过GPL协议开源
2001:SGI放弃支持,德拉华大学将编译器重新命名为open64
2001-2004: 计算所和Intel开始合作开展针对安腾的ORC
2003:PathScale开始支持x86后端的移植工作
2004:四月PathScale X86编译器合并
3,Open64的重要特性
在当今的产品级编译器中是最先进的设计
架构设计的重点放在优化实现上
1996年作为产品级编译器,2000年开源
兼容gcc/g++并能和其交互工作,包括源码结构,命令行,ABI和装载
易扩展和增强
易移植到新处理器
被广泛使用在当今的很多优化研究中
适合小团队的开发环境
4,PathScale编译器的重要贡献
移植到X86/X86-64
从2004年开始一直是x86 64位中性能最好的
在GNU和Open64之间建立了桥梁,以便方便的跟随GNU的发布
拥有OpenMP运行时库的专利权
质量评价和编译机制
5,Open64总体设计
全范围优化兼容的编译器架构
重要组成—-一个通用的中间表示WHIRL:
a,支持多前端的独立后端
b,一种中间表示,多层表示
c,编译处理过程即为中间表示不断降低的过程。
给予优化范围的特定成分:
a,LNO:Loop-oriented,基于数据依赖的优化
b,WOPT:基于SSA的全局范围优化
c,IPA:inter-procedural,过程间优化,需要对整个程序的分析
d,CG:代码生成,依赖于目标机
在不同阶段之间无重复工作:
a,所有阶段都可调用WHIRL简化函数
b,共享的分析结果
c,彼此调用来完成工作
6,编译器中中间表示(IR)的角色
支持多前端
支持多处理器
进行优化变换的中介
各个阶段间的通用接口
促进编译器设计中的模块化
减少多余功能
现代编译器中的关键设施
7,IR的语法层次:
从高到低,高层次靠近源程序,低此次接近机器指令
在较高层次中:
a,较多样的组成
b,较短的代码序列
c,较多的程序信息表示
d,分层次的结构
e,不能进行很多优化
在较低层次中:
a,较少的程序信息
b,较少的结构种类
c,较长的代码序列
d,较平坦的结构
e,所有的优化都能进行
8,Open64的WHIRL IR
WHIRL由SGI的Open64小组开发:
a,一种IR,多层表示
b,编译过程即不断降低表示层次的过程
c,每个优化都有其最适合做的表示层
d,共享分析结果
e,阶段之间没有冗余操作
9.编译过程:
源码->前段(FE)->高层优化(VHO)->过程间优化(IPA)->循环嵌套优化(LNO)->全局优化(WOPT)->代码生成(CG)->汇编码输出
对应中间表示:
源码->非常高层WHIRL->高层WHIRL->中间层次WHIRL->较低层WHIRL->目标机指令->汇编码输出
10,优化设计的总原则
在能榨取转换机会的最高层次做优化,即在尽可能高层次做优化
a,有更多能协助分析的源程序信息
b,需要操作的代码序列较短
c,对于相同的计算,变更最少
将使得:
a,实现代价较小
b,更快更高效的优化
c,更稳定(便于测试和debug)
11,阶段次序设计原则
较低层处理的需求:在较低层次暴露的优化机会需要在较晚时做
能暴露优化机会的较早的阶段:内联(Inline),常数传播,循环合并
能为较后阶段计算有利信息的阶段:别名和指针冲突消除,使用-定义关系,数据依赖
能规范代码的较早阶段:相比其他阶段对代码的变动较小的,不能提高程序性能de,依赖较后阶段清理操作的
会被使用多次的小代价优化
将破坏源程序信息的优化尽可能的放在后面
12,WHIRL的设计
可执行代码的WHIRL树节点:
a,WHIRL节点在common/com/wn_core.h文件中被定义
b.每个函数体被一个大树表示
用于定义的WHIRL符号表:
a,不同的表用于表示不同的声明结构
b,参见common/com/symtab*.h文件
最小的WHIRL节点是24字节
使用了域压缩来节省空间
二进制读写—WHIRL文件是ELF文件格式的
不同的阶段有唯一的WHIRL文件后缀:
a,前端:.B
b,IPA/内联器: .I
c,LNO: .N
d.WOPT: .O
ASCII 翻译器
13,重要的WHIRL概念
operator:操作的名称
desc:操作数的机器类型(标量)
rtype:结果数的机器类型(标量)
opcode:三元组(operator,rtype,desc)
symbol table index:一个源码中符号对应的唯一标识符
high level type:和源码声明相同的类型结构–对实现ANSI别名规则很重要(?)
field-id:在一个结构体或union中唯一的标识符,较新的为X86 MMX/SSE定义的128-位SIMD机器类型
14,优化中的WHIRL概念
语句节点是序列点(? Statement nodes are sequence points)
a,仅在语句边界可能有副作用
b,有副作用的语句有:Stores(存储),Calls(调用),asms(汇编?)
表达式节点不是序列点
表达式执行无副作用的计算–允许激进的表达式转换
源码位置信息(为了调试需要)仅应用于语句节点
15.WHIRL 映射(?)
注释型WHIRL节点有附加信息
解决WHIRL节点大小固定问题
对临时信息很有用
WHIRL节点归类
Map_id存储在每个WHIRL节点中–同一类中map_id唯一
信息存储在map 表中,并以map_id作为索引
16,更高层WHIRL(Very High WHIRL)
维持源码中的抽象表示
能在损失较少语义的情况下转回C/F90
最初被使用于FORTRAN 90,随后扩展到C/C++中
尽在VH WHIRL中允许的结构:逗号操作符,嵌套函数调用,C选择符(?和:),F90中的triplet,arrayexp,arrsection,where
内联器能够工作在此层
17,高层WHIRL
支持循环级优化的结构
固定的控制流(虽然还不精确)
关键结构:ARRAY(数组,数据依赖分析和向量化),DO循环,IF语句,FORTRAN I/O语句
IPA,PREOPT和LNO工作在此层
能转换回源语言—允许用户看到内联和LNO的效果
18,中间层WHIRL
一对一映射到RISC指令
通过jumps(跳转)实现精确的控制流
地址计算暴露出来(因为无ARRAY了?)
位域访问暴露
复数被扩展为浮点数操作
WOPT在此层工作—统一的表示提升了优化的机会
19,低层WHIRL
为了便于在CG转换为机器指令而出现的最后的WHIRL形式
一些内在intrinsics(?)使用calls(调用)代替
遵循连接协议的形式暴露(?)
数据布局结束
此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://lingcc.com
若需要此讲义的原版,请email我
Fred Chow 原版讲义见最后一页
GNU的PLUS树节点有14个域,gspin根节点来编码PLUS,再加上13个表示剩余域的gspin节点
tree_code = PLUS tree-code_class = BINARY tree_type tree_chain = NULL flags arity = 13 file name line no operand 0 operand 1 unused unused unused unused
此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://lingcc.com
若需要此讲义的原版,请email我
原版讲义见最后一页
- Open64中的内联器
IPA中的内联器(ipa/main/analyze)
轻量级内联器(ipa/inline,ipa/main/analyze)–均提供死函数删除
独立的内联器:前身是轻量级内联器,比轻量级内联器具有更多的功能,当前没有被使用
GNU前端的内联器被完全禁用
- 轻量级内联器
独立执行
针对单个文件工作- 无跨文件内联
在-ipa编译时不会调用
在-O3且无-ipa时总被调用
对从头文件中修剪无用函数很重要
阶段开始时会将得到的信息汇总
输出文件为.I
- IPA中的内联器
程序范围越大内联器功能越强大
使用IPL中创建的信息
支持递归内联
此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://lingcc.com
若需要此讲义的原版,请email我
Fred Chow 原版讲义见最后一页
定义:当多个Use-def边要经过同一个bb时,创建一个φ节点,让所有的依赖边都经过它。
不再需要精确的指定use-def边
(译者注:此处为了简便起见,略去了原作者的例子)
转载自:http://www.lingcc.com/2009/11/30/10168/
此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://www.lingcc.com
Fred Chow 原版讲义见最后一页
方法:
需要两遍SSA构造:
(译者注:此处略去Fred Chow的例子)
(译者注:此处略去Fred Chow的例子)
两个阶段:
(在be/opt/opt_bdce.cxx中实现)
遍历整个程序:
SSA为1和2提供了解决方法。1前面已经讲过,2属于Main-OPT阶段
Pre-optimizer
Main optimizer
转载自:http://www.lingcc.com/2009/12/13/10273/
此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://www.lingcc.com
Fred Chow 原版讲义见最后一页
can_be_avail给出最优化计算的可能插入点
将所有的PHI初始化为can_be_avail
将所有非downsafe且有至少一个操作数为步骤二中重命名的操作数的PHI节点标记为not-can_be_avail(边界条件)
将not-can_be_avail属性沿定义-使用边向上传播给非downsafe节点
第二部分:实现活跃区间最优化
找到最新的插入点
插入标准:
操作数步骤二中插入的
步骤五:最终化
步骤六:代码外提
引入真正的临时变量t
转换整个程序
(译者注:在介绍SSAPRE算法的过程中略去了Fred Chow讲义中的两个小例子)
子树无冗余说明树的祖先部分也无冗余
(译者注:此处略去了Fred Chow讲义中的小例子)
(译者注:此处略去了Fred Chow讲义中的小例子)
执行结果是不完全的死存储删除
基于反向CFG
需要静态单使用形式(Static Single Use ,SSU)
(译者注:此处略去了Fred Chow讲义中的小例子)
(译者注:此处略去了Fred Chow讲义中的小例子)
基于全局值编号的冗余
PRE仅仅识别词法上独立表达式之间的冗余
能从非语法独立表达式中提取冗余
全局值编号能识别计算相同值的非独立表达式
Open64的GVN算法基于Taylor Simpson的论文
GVN之后,在相同值编号的表达式之间删除冗余
基于值编号的完全冗余删除(VNFRE)
PRE中设计的表达式不计算相同俄值,PHI在流图汇合点将不同的值合并
在计算相同值的表达式之间尽可能存在完全冗余
VNFRE消除具有相同GVN的表达式之间的完全冗余
强度削弱
归约表达式—归约变量的线性函数(?)
强度削弱使用归约变量的递增代替归约表达式的计算
反向循环规范化(?)—定义:归约表达式提升为归约变量过程中可能会出错
此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://www.lingcc.com
Fred Chow 原版讲义见最后一页
唯一在程序间的优化操作。分析:收集整个程序的信息; 优化:在程序过程之间进行优化。IPA的整个优化效果取决于它之后的优化;IPA也为之后的优化阶段提供了跨文件的信息。
即:Pre-IPA(IPL)阶段。将前端扩展为一个编译单元。它在PREOPT阶段之后成为VHO(?),并将所有优化前的WHIRL信息汇总。之后生成含新的ELF段的伪.0文件。这个.0文件包含WHIRL段和汇总信息阶段。
使用汇总信息的目的是OPA不需要沿着WHIRL传播;每个PU中存储的信息包括:PU内容的统计、每个PU中的反馈信息、形参、全局变量赋值、PU中的调用点、实参、常数表及其值、IPA相关表以及它们的mod/ref(?)、相关符号的SSA图、指定的简单表达式和语句、控制依赖、常见块和规模(?)、结构体访问信息(详见ipa/local/ipl_summary.h文件)
它使用了pre-linker的机制:IPA阶段编译到ipa.so文件中,ipa.so使用成为ipa_link的GNU ld实现链接。操作的模式:首先,传递所有输入文件,进行符号表解析,读入汇总信息;然后进行过程间分析,仅工作在汇总信息上、再接着进行过程间优化,即读入WHIRL并修改WHIRL;最后得到输出。
ipa_link和ld(无-ipa选项时)在link阶段看到的对象相同。他们的ipa_link和ld符号解析规则也一样。不同文件中的全局符号将会合并到一个单一的全局符号表中,并保存在symtab.i文件中,且之后的全局符号表只有一个。
只要没有特定到单个PU的符号表都将合并,顶层的驱动函数是IPC_merge_global_tab(common/ipc_symtab_merge.cxx),并且创建了一个新表用来将旧表中的旧索引映射到新索引。符号表合并的顺序依次是:字符串表、类型、TCONs、符号、INITOs和INITVs、符号属性(ST_ATTR)
此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://www.lingcc.com
Fred Chow 原版讲义见最后一页
该机制来自Cydrome,并进行了增强。将目标机指令集,ABI和调度信息参数化。通过表生成机制来编译和链接,生成的表是用于CG阶段的C++文件(?).这种机制能将优化算法和体系结构细节分开,而且再移植到新结构上时能最小化编译器的改动,因为机器相关的内容存放在机器相关的文件夹内。支持ISA子集。不同处理器的调度信息实现编译成独立的.so文件,并使用编译选项控制dlopen()使用哪个.so文件
这种中间表示的每个操作(op)对应一条机器指令,通过targ_info来格式化指令。在一个目标机op中操作数和结果都存放在TN中(或者寄存器符号中).TN有符号、直接量和寄存器三种类型。TN的类型都是依据指令格式制定的。每个操作都使用两个操作数,并写出一个结果(和RISC相似),某些特殊的指令也能写两个结果(如 mul)
这部分的作用是将WHIRL展开成CGIR中的操作(whirl2ops.cxx,cgexp.cxx,x8664/exp*.cxx).TN的创建是用来存储中间结果。并且采用单赋值TN的形式来最小化依赖。CG阶段接下来的工作都在CGIR上做。每个机器op包含有指向WHIRL的指针,用来做别名分析和依赖信息查询,整个CG展开部分是机器相关的。对于X86,在LRA之前做展开,这样能通过创建拷贝指令,增强两个操作数之间的指令错左。如r=s+t —> r=s,r+=t。通过这种方式能将LRA中的寄存器合并,避免不必要的拷贝。
即在扩展的基本块内做窥孔优化(ebo.cxx).什么是扩展基本块?即由串联的基本块构成,单个入口,可以有多个出口边。所做的优化包括:向前传播,通用子表达式删除,常数折叠,死代码删除(冗余load和store删除),机器相关优化(如x86中的load-执行指令优化,add使用LEA指令)
首先,创建TNINFO来跟踪TN值,每个TN都有独立的TNINFO,能够跟踪TN值可用,识别定义TN值操作等,使用较早的TN替代操作数中被移除的TN。其次创建Op哈希表,将相似的op哈希到同一个表段中,允许快速识别”匹配”op,若两个op访问同一块内存则视为它们存储匹配,这样能删除多余的存储访问。如果两个op完成相同的功能,则视为ALU匹配,这样能做通用子表达式删除。该EBO优化在CG中被调用了多次。
控制流图在CG展开之后创建,所做的优化有以下几个(cflow.cxx):分支删除,将常条件分支转换为goto指令,将分支到分支,分支到goto,分支到return的指令折叠;热基本块定位,使用反馈信息评估或者用户提示的方式找到热基本块,重新排列基本块,使得流图尽量直线少跳转;增长基本块链(?);通过复制基本块来减少分支(尾部冗余),不可到达代码删除。
跨迭代循环优化(?)(cio_rwtrans.cxx)、最内层循环展开优化(cg_loop.cxx)、预取修剪优化
删除数组元素跨迭代边界带来的冗余,构建循环体中指令访存的依赖关系,引入变量omega表示循环迭代间依赖到距离。
如果变量的活跃区间交叉,则插入寄存器移动,在循环入口前初始化临时变量。
从写内存中读入冗余
使用较小的omega开始跟踪冗余指导寄存器压力达到某个阈值。
写写冗余
需要在循环出口插入剩余store
建立在跨迭代读冗余删除操作和减少寄存器压力操作的基础上
在基本块范围内进行(hb_sched,cxx),基于WOPT的别名信息和LNO的依赖信息来创建依赖图(cg_dep_graph.cxx).指令调度和寄存器分配相互关联,好的寄存器调度可能增加寄存器压力。CG阶段的指令调度执行两次:第一遍用来评估良好的指令调度前提下的寄存器需求情况,之后全局的寄存器分配器将许可每个基本块所需要的寄存器数,紧接着进行在寄存器分配后进行第二遍调度。寄存器分配之后,就能将依赖建立在真实的寄存器上,使用列表调度算法(list scheduling algorithm),这种策略在向前和向后两个方向上都能进行。
通过最佳化使用可用的物理寄存器提升执行性能,在需要的时候生成溢出代码。需要遵守ABI和ISA中关于寄存器的使用:参数寄存器,函数返回寄存器,被调用函数保存寄存器(在过程进入或退出的时候保存或弹出值)、调用函数寄存器(在调用时保存或弹出值)、在某些特殊指令中所使用的特殊寄存器。
依据targ_info的寄存器文件参数执行,应用在寄存器TN(即符号寄存器)上,活跃分析能识别全局的TN(GTN),这些GTN的活跃区间将跨基本块(gra_live.cxx),全局寄存器分配(GRA)采用下面的方式应用在GTN上(gra_mon/*.cxx):基本块粒度的分配,使用基于优先级的图着色算法,允许指定数量的寄存器给LRA。本地寄存器分配(LRA)采用下面的方式使用GRA允许数量的寄存器在每个基本块之间分配(lra.cxx):指令级的粒度,仅在指令间向后遍历一次。若无法使用GRA,则通过插入溢出代码将GTN转换为本地的方式来本地化。
使用的是传统的数据流分析,使用基于每个BB中TN数的位向量,通过传递以下信息来设置本地信息:live_use—本地向上展开使用的TN;defreach_gen—BB中定义的TN。使用数据流来计算:defreach_in—-能够到达bb顶端的定义, defreach_out–能够到达bb下端的定义 live_in—-在bb顶端使用向上展开 live_out—在BB下端使用向上展开。 在BB i中,全局TN的集合由 defreach_in i and live_in i or defreach_out i and live_out i 给出
使用粗粒度的干扰方式,分配是以BB为单位进行的,每个BB中,每个寄存器赋值为1个TN,活跃区间也由BB构成,若两个TN的活跃区间重叠,则视为干扰。这种GRA算法是域驱动的,即在BB之间管理寄存器资源,当有可用寄存器时,就将活跃区间分割用来匹配域。这种算法无回溯。
LV即活跃区间,LU即一个活跃区间(活跃单元)内的单元(BB).因此一个LV指向它的LU列表。为了能测试干扰,我们使用BB(基于BB号)的位容器,称之为BBmem,如果两个LV的BBmem交叉则视为它们有串扰。 所有的LU默认信息为:0 use,0 def,0 call,无引用等,此时不需要LU节点的分配。干涉图即一个所有LV之间两两有干扰的指针链表
通过比较是否分配时指令数的不同。若不分配,每个TN本地向上扩展的BB中,一个load指令,在每个有TN定义的BB中,一个store指令;如果分配,可能需要重新在活跃区间入口载入,也可能从活跃区间出口处溢出。于是每个BB的获利=避免的load指令数/避免的store数*频率,每个BB的代价=需要的溢出数/重载入数*频率。每个LV的优先级=每个bb中TN的获利和代价之差的总和/BB总数。
调用者保存的寄存器–需要在调用中保存/重存;被调用者保存的寄存器—需要在PU首次使用情况下进入/退出时保存/重存;参数寄存器–为用于传参的TN优先分配(负代价).函数返回寄存器–优先分配给存函数返回值的TN
定义:无约束LV:干扰图中的邻居数小于能赋值的寄存器数的LV。具体算法如下:首先计算每个LV的禁止赋值寄存器(forbidden)集合,将LV分成无约束和约束两类。对于每个寄存器类,重复下面的步骤知道所以非溢出LV都被分配:1,计算所有新LV的优先级,若为负则溢出;2,将未分配的LV设置为最高优先级LV-best;3,对于每个在LV-best的禁止赋值寄存器集合(~forbidden(LV-best))中的寄存器r,计算其分配代价Regcost(LV-best,r);4,将最小Regcost定位r-best。5,若LV-best的优先级(Priority(LV-best))小于Regcost(LV-best,r-best),溢出LV-best,否则,将r-best赋值给LV-best;在干扰LV-best的LV列表中,更新禁止赋值寄存器集,分离出无法着色的LV,溢出无法分离的LV,最后将寄存器分配给无约束的LV。
一是为了增强着色性,二是为了改变活跃范围的大小。对于前者,当forbidden(LV)=寄存器集合数时分割,若当出现TN处,它的所有BB中无可用寄存器时,将真个LV溢出。对于后者,自动将非相邻的块和lV分开,当被调用保存寄存器调出时分割,被分割出的域若0出现,则溢出.
尽最大可能将LV-new分割出LV-orig从而使forbidden(LV-new)不至于满(?).保留下来的LV-orig或者能分配或者不能.依照控制流图(不计回边)的拓扑序管理LU表.
具体算法:
1. Go through LU list to pick LU such that TN appears and register is available
2. For each LU in LV-new:
For each BB-succ of LU
If BB-succ is member of LV-orig, and adding BB-succ to LV-new does
not cause forbidden(LV-new) to become full,
Move BB-succ from LV-orig to LV-new;
Update forbidden(LV-new) and LV-new’s LU list
该过程是在一个向后遍历BB中的指令过程中将寄存器分配给本地TN的,BB的每一类可用寄存器集合在这个过程中飞速的更新.在一个向后过程中,对于每个指令,先处理结果TN再处理操作数TN;每个本地TN最先考虑的是在该TN的活跃范围内的最后一次使用;采用时间片轮转的方式给TN赋寄存器;对于出现在结果中的本地TN,通过将该寄存器添加回可用集合的方式来释放;相同的寄存器可以分配给同个指令中的结果和操作数,这种情况下,寄存器移动的指令可以直接删除.
需要在LRA开始阶段为每个BB增加带额外拷贝的额外TN.为遵守两个操作数格式:
TN100=TN101+TN102
变为
TN100 = TN101
TN100 = TN100 +TN102
为遵守8位寄存器子类:
TN100 = sete ….
变为
TN101 = sete …
TN100 = TN101
TN101是8位寄存器子类,TN100没有这种约束。
一些指令需要特定的寄存器,需要为rax,rcx和rdx创建单寄存器子类。引入涉及严格遵循寄存器子类的TN拷贝
如
TN100 TN101 = mul32 TN200 TN201
变为
eax = TN200
eax edx = mul32 eax TN201
TN100 = eax
TN101 = edx
因为每个TN必须分配一个寄存器,当无可用的寄存器分配给TN时,就调用Fix_LRA_Blues()来释放额外的寄存器,或者尝试重新LRA这个BB。对于函数Fix_LRA_Blues(),k可以使用不同的策略。主要有以下三种策略:溢出一个已经通过GRA分配的寄存器;重新做指令调度来减小寄存器压力;溢出一个先前分配且超过其活跃范围的寄存器。
x87仅在-m32下支持,x87栈寄存器视为普通寄存器文件建模,在最后的指令调度阶段,将X87寄存器转换为类似栈的操作(x8664/cg_convert_x87.cxx);在BB之间转换时,栈都被维持在相同的状态。需要插入fxch指令。如果寄存器栈不再活跃,就使用X87指令中的弹出版本(pop version)
该过程在本地寄存器分配和最终指令调度阶段之间进行(gcm.cxx);计算每个基本块中的关键路径;通过移动指令来调整基本块的方式来寻找缩短关键路径的机会,如将开始指令移动到基本块的前驱,将结束指令移动到基本块的后继。
在WHIRL中,使用ASM_STMT表示一个汇编语句,ASM_STMT的输入输出维护着WHIRL的接口。在CG中,展开成汇编伪操作(whirl2ops.cxx).输入和输出的接口也转换为了TN。接下来TN通过GRA/LRA赋予真实的寄存器。在代码输出阶段,使用被赋予的寄存器替换汇编代码串中的操作数(cgemit.cxx,x8664/cgtarget.cxx)
转载自:http://www.lingcc.com/2009/12/21/10379/
循环嵌套优化(LNO)
此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://www.lingcc.com
Fred Chow 原版讲义见最后一页
循环嵌套优化(LNO)概述
该优化主要在嵌套循环上做转换。该部分工作的范围时每个顶层循环内的嵌套,优化分析过程中并不构建控制流图,而是通过数据依赖分析驱动。使用标量优化阶段(WOPT)提供的别名和使用-定义信息,并通过代码生成器将数据依赖信息附注在每个use上(仅在最内层循环),这部分优化需要对硬件资源建模。
依赖的定义:给定两个引用R1和R2,若它们都访问同一块内存且从R1到R2有路径存在,则称R2依赖于R1,依赖分为:真依赖,反向依赖(anti dependence)和输出依赖三种。另外还要说明访问数组和向量(vector)的区别,访问数组当每个数组的下表时循环归纳变量时,访问向量时所有的向量下标都访问数组。依赖测试(输入是访问数组时),参考论文<高效精准的数据依赖分析>(Efficient and Exact Data Dependence Analysis),Dror Maydan,et al.., PLDI’91. 依赖测试的输出是依赖向量,每一维表示一个循环嵌套的层。
数据缓存转换(?),协助其他优化的转换,向量化和并行化
通过缓存分块实现,即将循环转换为工作在能装在cache中的子矩阵上。此外还有循环交换,数组填充(减少缓存冲突),预取生成(隐藏cache失效访问的长延时)和循环合并及分裂等转换。缓存分块如下
for (i=0; i
for (j=0; j
for (k=0; k
c[i][j] = c[i][j] + a[i][k]*b[k][j];
矩阵B会一直出现缓存失效,总共失效n^3+2*n^2次(不考虑矩阵行大小).若使用能整个装入cache的子矩阵,如下图所示
则C11 = A11 * B11 + A12 * B21。对于b大小的子矩阵,失效次数为(n/b)*n^2+2*n^2
增加数据的局部性
如
DO I = 1, N
DO J = 1, M
A(I, J) = B(I, J) + C
此时,没有空间重用,对于每个A和B的引用都将cache失效
DO J = 1, M
DO I = 1, N
A(I, J) = B(I, J) + C
每进行16次循环才会失效一次(假设数组元素为4字节,缓存行大小为64字节)
(译者注:此处的例子是Fortran语言,该语言二维数组在内存内按列存储)
这里面的转换为单模转换,并将其和缓存分块,循环翻转和循环偏移等结合起来增强循环嵌套的总体数据局部性。并实现自动向量化和并行化。
软预取主要考虑一下三个方面:取什么,何时取,避免无用的预取。对于第一点,仅对最可能引起cache失效的引用取;对于第二点,确保不早也不晚;对于第三点,综合考虑寄存器压力,缓存污染和存储带宽进行。我们的预取引擎的主要阶段:先Process_Loop–构造内建结构等;之后Build_Base_LGs–建本地化组(即可能引用相同缓存行(cache line)的),然后Volume–从最内层到最外层计算每个循环的数据量;Find_Loc_Loops–决定那个本地化的组需要预取;Gen_Prefetch–生成预取指令。
提前预取N个缓存行:对于a(i),预取a(i+N*line_size),使用选项-LNO:prefetch_ahead=N(默认为2)来控制。每个缓存行一个预取指令,这里牵涉到两个问题,循环中的版本化以及和循环展开的结合。对于前者,可以转换如下
DO I = 1, N
if I % 16 == 0 then
含预取指令的循环体
else 不含预取指令的循环体
对于后者,转换为:
DO I = 1, N, 16
prefetch b(I + 2*16)
a = a + b(I)
a = a + b(I+1)
...
a = a + b(I+15)
循环分解:使得循环交换,向量化和并行化成为可能,减少冲突失效。对于循环合并:减少循环开销,增加数据重用,增大循环规模。
标量展开和数组展开:减少循环内依赖,使并行化成为可能;标量重命名:循环嵌套就能分别优化,减少寄存器分配时的约束;数组标量化:改进寄存器分配;此外还有提升混乱的循环边界、最外层循环展开(该过程可以形成更大的循环体,减少循环开销)、数组替换(前向和后向)、IF语句提升(通过重复匹配的迭代来代替循环)、循环反转换(即将循环内无关的条件判断移出)、聚合-分散(?)、提升不变数组引用到循环外、迭代内CSE
(译者注:本段提到的几个优化,Fred Chow都有例子在ppt中,本人偷懒没有附上,请到本文最后一页的原版ppt中参阅第13-18页)
并行化包括三部分:SIMD代码生成(高度依赖目标机的SIMD指令),生成向量intrinsic(基于可用的库函数)以及自动并行化(受其余后端部分OpenMP支持的影响)。
应用在最内层循环,没有牵涉在一个依赖环中的任何语句都可能会有向量化机会
通用向量化的实现首先需要约束检测,之后要对最内层循环做依赖分析,包括构建语句依赖图,检测依赖环(强联通分量,SCCs);然后使用实现向量化的技术,在有依赖环的地方使用(参见下面),最后将循环重写为向量化的循环。
标量展开
DO I = 1, N
T = a(I)
a(I) = b(I)
b(I) = T
ENDDO
将标量T展开为数组t后
DO I = 1, N
t(I) = a(I)
a(I) = b(I)
b(I) = t(I)
ENDDO
DO I = 1, N
a(I+1) = a(I) + C //依赖环
b(I) = b(I) + D //无环
ENDDO
转换为
DO I = 1, N
a(I+1) = a(I) + C //有环,不可向量化
ENDDO
DO I = 1, N
b(I) = B(I) + D //无环,可向量化
ENDDO
其他的机制L循环交换,数组重命名等
SIMD是向量化的特殊形式,对数组的访问要求连续,如A[2*i]和A[2*(i+1)]就不连续,对于F90,可能需要循环变体来确保连续性。此外还有对齐问题,有时因为没有对齐到128位边界可能没有任何优化效果,可能还需要去掉一些层。另外,可能需要剩余循环
DO I=1,N
a(I)=a(I)+C
ENDDO
转换为
DO I=1,N-N%4,4
a(I:I+3) =a(I:I+3)+C
ENDDO
!remainder
DO I =N-N%4,N
a(I)=a(I)+C
ENDDO
可能需要对每个计算单元复制累加器
为了分离出intrinsic,通常需要循环分解,如
for(i=0;i
a[i] = a[i]+3.0;
u[i] = cos(ct[i]);
}
转换为
vcos(&ct[0],&u[0],N,1,1);
for(i=0;i
a[i]=a[i]+3.0;
预优化 Pre-optimization
完全短循环展开 Fully Unroll Short Loops (lnopt_main.cxx)
构建数组依赖组Build Array Dependence Graph (be/com/dep_graph.cxx)
多种优化 Miscellaneous Optimization:提升易变的循环下界 hoist varying lower bounds(access_main.cxx)、形式化最小/最大值 form min/max(ifminmax.cxx)、死存储删除数组 dead store eliminate arrays(dead.cxx)、数组替换(前向和后向) array substitutions(forward and backward)(forward.cxx)、循环翻转 loop reversal (reverse.cxx)
循环反转换 Loop unswitching(cond.cxx)
缓存分块 Cache Blocking (tile.cxx)
循环交换 loop interchange (permute.cxx)
循环合并 loop fusion (fusion.cxx)
提升混乱的循环便捷 Hoist Messy Loop Bounds(array_bounds.cxx)
数组填充 Array Padding (pad.cxx)
并行化 parallellization (parallel.cxx)
束缚 Shackling (shackle.cxx)
聚合发散 Gather Scatter (fis_gthr.cxx)
循环拆分 Loop Fission (fission.cxx)
SIMD (simd.cxx)
提升IF语句 Hoist IF (inopt_hoistif.cxx)
生成向量intrinsics (vintr_fis.cxx)
生成预取指令 Generate Prefetches (prefetch.cxx)
数组标量化 Array Scalarization(sclrze.cxx)
循环无关量外提 Move Invariant Outside the Loop (minvariant.cxx)
循环迭代内通用子表达式删除 Inter-Iteration Common Sub-Expression Elimination (cse.cxx)
支持反馈指导优化(Feedback-directed Optimizations)
此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://www.lingcc.com
Fred Chow 原版讲义见最后一页
使用目标机器可执行代码来收集程序运行信息并反馈给编译器(FDO),实现步骤:首先,使用选项-fb_create fb_file来插桩编译程序;然后运行“training run“,非常慢;最后再使用选项-fb_opt fb_file来编译程序。另外要求除fb之外的其他选项都是一样的。
插桩在编译阶段的WHIRL级中进行。数据手机工作可以在反馈编译的同一个点进行,可以收集到精确的程序代码反馈数据,且精确性不受编译选项影响(只要这些选项在两次编译时时相同的),这些插桩的代码是对运行时库libinstr.{so,a}的调用。并且支持多种类型的反馈数据—BB热度,边热度,值轮廓(?profiles)
可以在编译过程不同的阶段插桩(和收集数据)使用的是选项(-fb_phase):0-在VHO前,1–在LNO前,2–在WOPT前,3–CG前。仅稍后一点的阶段才能使用收集到的信息。另外当代码转换的时候,收集的数据也需要更新–提供了一些完成该功能的函数,并有一些参测的功能。默认的插桩阶段时-fb_phase 0.PathScale并未对其他的-fb_phase做测试
为某些常用的运行时值调优
OpenMp和自动并行化
此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://www.lingcc.com
Fred Chow 原版讲义见最后一页
两种在open64中获得粗粒度并行化的方法:OpenMP和自动并行化。前者使用选项-mp来指导并行化,在Fortran,C和C++中支持OpenMP2.5。后者需要选项-apo打开,功能是在LNO阶段检测并行的循环并插入指导。在同一个编译中,可以两个同时使用。
并行化的代码给出它自己函数的轮廓(?),轮廓函数嵌入到原始过程中,原始过程中的本地变量采用静态链的方式访问,指向轮廓函数的指针被传到libopenmp中的同步机制中用来做定型执行的调度。其中会有一段并行的代码保存下来以备串行执行之用
通过大量产生线程来达到执行并行代码区域的效果,默认情况下的线程数是CPU的个数,libopenmp(PathScale专利)包含:线程控制和同步机制和OpenMP intrinsic相关机制.运行时的动作通过环境变量控制,如指定线程和处理器之间的亲和性
首先前段将OpenMP直接翻译成WHIRL OMP编译指示和范围节点,然后OpenMP预lower(be/be/omp_lower.cxx),这一步在VHO后IPL前执行,对OpenMP的编译指示做初步的lower–使用OpenMP库中的例程调用代替intrinsics;接着做MP lowering(be/com/wn_mp.cxx),这一步在LNO之后WOPT之前执行,并行区域被扩充到多个例程中,每个例程都嵌入在原始函数中,并包含向上的对本地母函数的引用,还要插入代码在运行时决定并行还是串行执行,若并行,则调用OpenMP运行时库来产生大量线程执行并行代码,若串行则执行剩下的串行程序代码.大多数的OpenMP编译指示在这一步被删除.
是指当应用在LNO相关的MP lowering时,默认便已是用较晚的MP,MP的lowering多应用在LNO之后,LNO在有MP编译指示时运行–MP编译指示的出现让LNO的转换更加保守.选项-OPT:early_mp=on作用在较早的MP上,LNO引用并行例程时,若无MP编译指示,则使更激进的优化成为可能,附加的调用可能会抑制一些优化,在-apo选项下并不起作用.