open64简介

Open64课程-简介,概述和中间表示

转载自: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(调用)代替
遵循连接协议的形式暴露(?)
数据布局结束


Open64课程-编译过程

转载自:http://www.lingcc.com/2009/11/19/10024/

此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://lingcc.com
若需要此讲义的原版,请email我

Fred Chow 原版讲义见最后一页

—————————————————

1.优化控制哲学

  • 低优化级别–更短优化时间,更安全优化,更精确的计算,较缓慢的代码产生速度
  • 高优化级别–较长优化时间,更激进优化,精确性让步于高性能,更快的代码生成
  • 通过大量选项较好的控制优化–PathOpt2(?)提供了很多帮助

2,优化标志与涉及的阶段

  • -O0(-g下默认)—前端和代码生成器,关闭所有优化
  • -O1–前端和代码生成器,仅作本地优化
  • -O2(默认)—增加WOPT和其余的CG优化
  • -O3 —增加LNO
  • -ipa(能在任何优化级别进行)–增加IPA
  • -Ofast—和-O3 -ipa -OPT:Ofast -fno-math-errno -ffast-math效果相同
  • -OPT:Ofast是-OPT:ro=2:Olime=0:div_split=ON:alias=typed

3,选项组

  • 选项通过编译器不同阶段或者通过特征分组
  • 通用语法–   -GROUPNAME:opt[=val]{:opt=[val]}
  • 一些GNU类型的选项直接映射过来 -march -ffast-math -ffloat-store -fno-inline
  • 组名称: -LIST: 用户列出(?)    -OPT: 优化   -TARG:目标机器 -TENV:目标环境  -INLINE后端内联  -IPA:过程间分析  -LANG: 语言特征 -CG:代码生成  -WOPT:全局范围优化 -LNO:循环嵌套优化

4,编译器驱动器的角色

  • 在open64/driver中实现
  • 处理所有的命令行中的选项
  • 调用所有的编译阶段–预处理,前端,内联器,后端(be,lno,wopt,cg),汇编器,连接器
  • 保持和GNU选项的兼容

5,编译器驱动器

  • open64/driver/OPTIONS  具体选项表,能将一个选项对应到另一个选项
  • 单个运行,多个软链接
  • arg[0]字符串用于识别语言
  • 查询编译相关的环境变量
  • 在-march=auto时查询主机处理器类型(默认)
  • 查询compiler.defaults文件来得到系统相关选项

6,C/C++前端历史

  • 2000年开源时使用GNU2.95前端–直接从GNU内部语法树转换成WHIRL,在Open64中嵌入相互独立的C和C++前端
  • 2004年升级到GNU3.3.1
  • 206年类似于虚拟机,定义.spin文件格式—-GNU编译器不再作为Open64的一部分维护,简化升级到每个GNU发行版本的工作,C和C++前段中冗余代码消除
  • 2007年3月移植GNU4.0.2前端
  • 2007年10月移植GNU4.2.0前端
  • 考虑了会增加的未来GNU语言(?)

7,用GNU编译器作为前端

  • 使用为X86-64配置的GNU编译器
  • 过去的方式:  C语言前端open64/kgccfe      C++前端open64/kg++fe       调用嵌入在GNU代码中WHIRL生成代码       c++需要将整个编译结果转换为汇编语言来结束数据转换(?)        c和c++前端中有冗余代码
  • 新的方式:gspin树节点–GNU语法树建模工具:   在libspin库中功能实现,   gspin树节点可以提取到.spin文件中   识别GNU语法树中阻止gcc编译的点(?)  gspin树节点由gcc/tree.c所产生的GNU语法数生成    open64/wgen 将 gspin语法树节点(.spin文件)翻译成WHIRL节点(.B文件) wgen中操作的形式在kgccfe/kg++fe之后进行建模(?)

8,Gspin树节点

  • 目的:将GNU树中的所有信息编码到.spin文件中
  • 8-字节大小的gspin节点作为构建程序块的基本单元:在libspin/gspin-tree.h文件中定义  表示GNU语法树节点中的一个信息域   相邻的gspin簇表示一个GNU语法树节点   重新表示机制定义在libspin/gspin-tel.h文件
  • libspin管理gspin节点的布局
  • gspin节点的 I/O通过mmap()实现
  • ASCII抽取琦–为防止无限递归每个节点仅被处理一次

9,gspin节点例子

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

10,FORTRAN前端


Open64课程-内联

转载自:http://www.lingcc.com/2009/11/20/10068/

此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://lingcc.com
若需要此讲义的原版,请email我
原版讲义见最后一页

  1. Open64中的内联器
      IPA中的内联器(ipa/main/analyze)
      轻量级内联器(ipa/inline,ipa/main/analyze)–均提供死函数删除
      独立的内联器:前身是轻量级内联器,比轻量级内联器具有更多的功能,当前没有被使用
      GNU前端的内联器被完全禁用

  2. 轻量级内联器
      独立执行
      针对单个文件工作- 无跨文件内联
      在-ipa编译时不会调用
      在-O3且无-ipa时总被调用
      对从头文件中修剪无用函数很重要
      阶段开始时会将得到的信息汇总
      输出文件为.I
  3. IPA中的内联器
      程序范围越大内联器功能越强大
      使用IPL中创建的信息
      支持递归内联



Open64 课程–全局标量优化(WOPT I) part 1

转载自:http://www.lingcc.com/2009/11/25/10127/

全局标量优化(WOPT)一–Pre-OPT part 1

此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://lingcc.com
若需要此讲义的原版,请email我

Fred Chow 原版讲义见最后一页

  1. 全局标量优化WOPT总览
      工作在函数域内
      构建控制流图
      进行别名分析
      将程序表示为SSA形式
      基于SSA的优化算法
      采用多阶段协同的方式达到最终优化效果
      优化顺序重新设计以实现性能提升最大化
      分成PreOpt和MainOpt两个阶段:PreOpt工作机制类似LNO和IPA的预优化前段(在高层WHIRL中)
      为LNO和IPA提供use-def(使用-定义)信息
      为CG提供别名信息
  2. 本节要点
      SSA基础
      一些基于SSA的优化
      WOPT中的别名分析
      WOPT的SSA表示
      在WOPT中表示别名信息
      将SSA优化一般化到任何存储操作
      符号(sign-)和零扩展(zero-extension)操作优化
      预优化(Pre-optimizer)阶段结构
      SSAPRE
      其他冗余方面的优化
      主优化(Main-optimizer)阶段结构
  3. 什么是SSA                http://www.lingcc.com/2011/08/13/11685/
      名字:静态单赋值形式(Static Single Assignment):整个程序中每个变量仅允许定义一次
      作用:用于程序中间表示中的内建use-def依赖信息
      Use-def:从变量的每个使用指向它的定义的一条单向边
  4. 直线代码(Straight-line code)中的数据依赖
      每个被使用的变量有且仅有一个定义
      直线代码一般为单赋值
      使用到定义(Uses-to-defs):多对一的映射
      每个定义支配所有它的使用
  5. 非直线代码(Non-straight-line)中的Use-def依赖
      多个使用和多个定义之间有依赖 :在转换成中间表示过程中代价很大,难于掌控
      使用SSA能够从繁杂的依赖中恢复直线代码的特性
  6. 算符φ
      很显著的减少依赖边个数
      一个φ节点被认为是一种定义(它的所有参数被认为是使用)
      采用这种方式将多个的引用转化为一个定义
      每个定义反过来支配该节点之后的引用

    定义:当多个Use-def边要经过同一个bb时,创建一个φ节点,让所有的依赖边都经过它。

  7. 用重命名方式表示use-def边

    不再需要精确的指定use-def边

  8. 将程序转换成SSA
      φ仅在定义支配边界出需要(即它停止支配处)
      支配边界基于控制流图已提前计算
      两个阶段:在每个定义的支配边界插入φ(递归式地);将所有支配边界内的使用重命名为定义名字(在前序遍历支配树的过程中管理更新存储变量版本的栈

    (译者注:此处为了简便起见,略去了原作者的例子)


Open64 课程–全局标量优化(WOPT I) part II

转载自:http://www.lingcc.com/2009/11/30/10168/

全局标量优化(WOPT)一–Pre-OPT part 1

此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://www.lingcc.com

Fred Chow 原版讲义见最后一页

  • SSA中的zero version
      目的:在尽量不影响优化效果的前提下降低SSA表示的代价
      • 放弃完整的带MayDefs的使用-定义链表示。
        使用特定的version-Zero Version:标记不完整的使用-定义信息,不要将其对应到单赋值中的属性

      方法:

      SSA和非SSA的version能够共存
      易失变量仅又有zero version
  • 识别zero version
      定义:Real occurrence–在构建SSA形式之前所有在程序中的出现。 对于虚变量,real occurrences 只它的非直接变量出现的地方。
      定义:Zero version–非真实出现在程序中但它的值是由至少一个chi节点或至少包含zero version的phi节点得到的标量version.
      初衷:在chi节点处引入zero version.
      使用-定义边在有zero version时是不完整的
      对于死存储删除,一个phi或chi节点,若它的结果是zero version,则不能删除
  • zero version算法

    需要两遍SSA构造:

      • 第一遍:先在WHIRL中表示,SSA的version存储在WN的ST_IDX域中,ver_stab 是SSA的version表(VER_STAB_ENTRY).将WHILR映射到相关的出现节点(OCC_TAB_ENTRY)来保存mu和chi节点。在第一遍SSA中,对于没有real occurrence的version,若由chi定义,或由ohi定义,且操作数中有一个为zero version则定义为zero version.使用一个worklist来遍历所有phi节点指导结果不再改变。
        第二遍:构建出HSSA,对于每个变量的zero version,仅创建一个coderep节点

    (译者注:此处略去Fred Chow的例子)

  • 死非直接存储删除
      直接应用SSA的死存储删除算法并不能识别很多死非直接存储。需要通过对虚变量的使用-定义链的分析来增强算法。
  • 非直接变量间的复写传播
      基于非直接变量节点定义域剧的指针,使用定义语句的r.h.s代替非直接变量;依据虚变量的使用-定义链能传播到更接近定义语句的地方(地址表达式要明显,确定无非直接存储地址间的交叉)
  • 概括SSA形式
      任何访存的结构都能表示为SSA
      高层次表示:数组集合,复合数据结构(结构体,类,C++模板)
      低层次表示:位域内的
      能够在它们上面应用基于SSA的优化算法
  • 针对结构体和域的优化
      大的结构体复制往往降级成循环实现,增加优化难度
      在结构体降级前应用SSA优化:死结构体拷贝的删除,结构体的复写传播
      域访问时考虑别名

    (译者注:此处略去Fred Chow的例子)

  • 位域的优化
      位域能够当成独立域实施更激进的优化
      SSA优化在域降级抽取或隐藏前应用:由于较小的覆盖区使关联的别名较少、和标量的表示相同
      降级到域抽取或隐藏之后:得到word范围的寄存器访问,减少存储访问、在标记操作时实现冗余删除
  • 符号扩展和零扩展优化
      动机:
      1. 符号/零 扩展操作在整数大小比操作大小更小的时候
      2. 在用户需要的时候也能进行:强制类型转换、截断
      对于安腾很重要:仅提供无符号loads,在指令系统结构中最常见的都是64位操作(绝大部分程序都是32位的)
  • 符号/零扩展操作:
      定义
      • sext n-符号位为n-1,所有>=n的位都合符号位相同
        zext n-n位无符号整数,所有>=n的位都为0
  • 符号扩展消除算法
      是SSA死代码删除算法的扩展(同时实现死代码删除)
      对每个变量使用一个标记活跃的位(而非一个单独的flag)
      对每个表达式树节点使用一个标记活跃的位
      1. 依据使用-定义边,计算边和控制依赖向后传播单个bit的活跃信息
      2. 删除操作

      两个阶段:

      (在be/opt/opt_bdce.cxx中实现)

  • 活跃bit传播阶段
      在表达式树中自上而下传播(从表达式结果到它的操作数)
      基于语句的语义,仅影响结果的操作数的位被标记为活跃
      在叶子节点,依据使用-定义边找到SSA变量的定义语句
      在没有新的活跃变量发现时传播结束
  • 无用操作的删除
      1. 赋值语句:如果该SSA变量没有活跃标记就被删除
      2. 其他语句:如果没被标记为必须,则删除
      3. 零/符号扩展操作:在以下两种情况下被删除–死bit标记(?)、冗余扩展(?)

      遍历整个程序:

  • 当dead位被标记时的操作
      与常数的位与操作:和0的位与操作标记为dead
      与常数的位或操作:和1的位或操作被标记为dead
      使用EXTRACT_BITS和COMPOSE_BITS(?)
      “sext n(opnd)”和“zext n (opnd)”:操作数n之后的高位标记为dead
      右移:被右移出的右bit标记为dead
      左移:被左移出的左bit标记为dead
      还有其他情况
  • 冗余扩展操作
      给定”sext n (操作数)”和“zext n (操作数)”下列情况将符号/凌扩展判定为冗余
    1. 操作数是<=n的小整数(通过较高位已知值)
    2. 操作数是整型常数
    3. 操作数是针对存储地址大小<=n的load
    4. 操作数是另外一个长度<=n的符号/零扩展操作
    5. 操作数是SSA变量:能否递归地通过使用-定义关系找到它的定义,分析它的r.h.s
  • 循环标准化(?)
      在连接多个阶段的过程中起作用
    1. 循环常规化:为每个循环插入一个人工索引变量,从0还是,间隔为1,递增
    2. 索引变量标准化:基于SSA图检测每个循环中的所有索引变量;在所用索引变量中,挑选一个主索引变量IV,对每个其他IV,在循环开始阶段插入使用主IV表示的赋值语句
    3. 复写传播:所有次要IV的使用都使用主IV的使用代替
    4. 死存储删除:删除所有次要IV的出现
  • 三种和依赖相关的优化策略
    1. 删除无用计算–死存储删除
    2. 删除冗余计算–通用子表达式、循环不变代码移动、部分冗余删除
    3. 计算重排序:循环转换、指令调度

      SSA为1和2提供了解决方法。1前面已经讲过,2属于Main-OPT阶段

  • WOPT阶段的优化
        • Goto转换
          循环常规化
          别名分析(流无关和流相关)
          尾递归删除(?)
          死存储删除
          索引变量标准化
          复写传播
          死代码删除
          为LNO和IPA计算定义-使用链
          将别名信息传送到CG

      Pre-optimizer

        • 基于SSAPRE机制的部分冗余删除:全局通用表达式、循环无关代码移动、强度削弱、线性函数测试代替
          基于值编号的完全冗余删除
          索引变量删除
          寄存器推销
          位域内的死存储删除

      Main optimizer

  • Pre-opt实现流程
    1. Goto 转换(opt_goto.cxx)
    2. 循环标准化(opt_loop.cxx:Normalize_loop())
    3. 创建优化符号表(opt_sym.cxx)
    4. 别名分类(opt_alias_class.cxx)
    5. 创建CFG(opt_cfg.cxx)
    6. 控制流分析(opt_cfg.cxx):支配节点、支配边界、后继支配、后继支配边界、不能到达代码、if语句转换、循环结构表示
    7. 尾递归删除(opt_tail.cxx)
    8. 流无关别名分析(opt_alias_analysis.cxx:Compute_FFA())
    9. 创建基于WHIRL的SSA(opt_ssa.cxx)
    10. 流敏感别名分析(opt_alias_analysis.cxx:Compute_FSA())
    11. 死存储删除(opt_dse.cxx)
    12. 查找zero version(opt_dse.cxx)
    13. 创建HSSA(stmtrep,coderep)(opt_htable.cxx)
    14. 索引变量标准化(opt_ivr.cxx)
    15. 复写传播(opt_prop.cxx)
    16. 将非直接变量展开成直接变量(opt_revise_ssa.cxx)
    17. 死代码删除(opt_dce.cxx)
    18. 迭代,直到不再有变化:
      1. 控制流转换(opt_cfg_trans.cxx)
      2. 更新SSA(opt_rename.cxx)
      3. 复写传播(opt_prop.cxx)
      4. 展开非直接变量为直接变量(opt_revise_ssa.cxx)
      5. 死代码删除(opt_dce.cxx)
    19. 如果有IPA或LNO
      • 获得WHIRL(opt_emit.cxx)
      • 创建定义-使用信息(opt_du.cxx)


  • Open64 课程–全局标量优化(WOPT II)

    转载自:http://www.lingcc.com/2009/12/13/10273/

    此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://www.lingcc.com
    Fred Chow 原版讲义见最后一页

    全局标量优化II-Main-OPT

    • 三种和依赖有关的优化策略(Re-cap?)
      • 删除无用计算—死存储删除
      • 删除冗余计算—通用子表达式、循环无关代码移动、部分冗余删除
      • 计算排序—循环转换、指令调度
      • 本节将会讨论前两种
    • 部分冗余删除
      • 什么是部分冗余—执行某些路径时的冗余计算
      • 方法:在非冗余路径上插入的计算导致的完全的冗余(相对于部分冗余)
      • 这样,完全的冗余就会被删除
      • 部分冗余删除比循环无关代码外提要好

    • 部分冗余删除算法
      • 懒代码外提是最好的PRE算法
        1. 最理想计算方式—没有其他的代码能使编译生成的二进制隐形更快更何况在执行过程中的路径跳转
        2. 活跃区间最优化—没有其他的最优化计算代码能获得临时变量更小的活跃区间
        3. 另外,不需要双向的数据流(?)
      • SSAPRE是能应用在SSA图上的PRE算法
        • 稀疏变量version的懒代码外提(?)
        • 理解算法需要更多的直觉
    • SSAPRE的动机
      • 完全冗余首先开始于在支配边界计算/转换为部分冗余(?)
      • 使用SSA解决冗余问题
        • 使用临时变量h对问题建模来为表达式构建SSA
          • h的定义:计算表达式并保存结果
          • h的使用:重新装载较早时保存的结果
        • 相同的SSA version表示相同的计算结果
        • 仅需要在phi节点的入边插入
        • PRE数据流分析在SSA图上进行
      • SSAPRE算法
        1. SSA构建
          1. 插入PHI
          2. 重命名
        1. 数据流分析
          1. DownSafety
          2. WillBeAvail
        1. 转换程序
          1. 最终化SSA
          2. 代码外提
      • 第一步:插入PHI
        • 如同SSA中的变量:在表达式的支配边界上
        • 附加规则:将变量的值转换为phi节点结果的变量值.
      • 第二步重命名
        • SSA的重命名一样对h赋予SSA中的version
        • 附加的规则:仅在一个变量相同的SSA version处有相同的h version
      • 第三步DownSafety
        • 仅能在表达式能被预测的PHI节点(downsafe)处插入—计算最优化的时候需要(?)
        • 将所有的PHI节点初始化为downsafe
        • 将没有出现又能到达出口的PHI节点定义为not-downsafe(边界条件)
        • 依据使用-定义边向后传播not-downsafe属性
        • 排除有真实出现的使用-定义边(通过has-real-use标记)
      • 第四步WillBeAvail
        • 目的:为PRE插入识别PHI节点
        • 使用两个沿使用-定义边的向前传播(?)
        • 第一部分:为实现计算最优化
          1. can_be_avail给出最优化计算的可能插入点

          2. 将所有的PHI初始化为can_be_avail

          3. 将所有非downsafe且有至少一个操作数为步骤二中重命名的操作数的PHI节点标记为not-can_be_avail(边界条件)

          4. not-can_be_avail属性沿定义-使用边向上传播给非downsafe节点

        • 第二部分:实现活跃区间最优化

          1. 找到最新的插入点

          2. 对于所有can_be_availPHI节点:later表示次要的,not-later表示最新的插入点
          3. 主要想法:若PHI操作数由真实计算定义,插入就不能是次要的,否则会引入冗余(?)
          4. 将所有任一操作数由真实计算定义且标记为not-laterPHI节点标记为can_be_avail(边界条件)
          5. 将此not-later属性沿定义-使用边向前传播
        • 插入标准:

          • WillBeAvail = can_be_availnot-later的交集
          • willBeAvailPHI节点处,满足以下两种情况之一时,插入PHI操作数
            • 操作数步骤二中插入的

            • has_real_use为假且此值由非WillBeAvailPHI节点定义
      • 步骤五:最终化

        • h整合为真正的SSA形式
          • 对于每一个real occurrence
            1. 如果occurrence是冗余的,设置reload
            2. 如果计算需要保存,设置save
          • 对所有标记为insertPHI操作数进行插入
          • 将操作数未定义的PHI节点删除
      • 步骤六:代码外提

        • 引入真正的临时变量t

        • 转换整个程序

        • tSSA形式从hSSA形式转换而来

      (译者注:在介绍SSAPRE算法的过程中略去了Fred Chow讲义中的两个小例子)

      • SSAPRE的实际实现
        • 从词法上相同的表达式列表中依次应用SSAPRE算法
        • 对于高度-1的表达式效果明显
        • 子树无冗余说明树的祖先部分也无冗余

      • PRE合理的扩展到loads或非直接loads
        • load PRE应该从LHS的出现中获得好处

      (译者注:此处略去了Fred Chow讲义中的小例子)

      • 死存储删除可以看作是L-值的冗余
        • L-值冗余中:
          • 使用使L-值无意义(?)—使store有意义
          • 较早的stores可以删除—导致反方向的冗余问题(?)

      (译者注:此处略去了Fred Chow讲义中的小例子)

      • store部分冗余删除(SPRE)
        • 执行结果是不完全的死存储删除

        • 基于反向CFG

        • 需要静态单使用形式(Static Single Use ,SSU)

          • 在分支上翻转PHI节点
          • 目前通过模式匹配在SSA实现

      (译者注:此处略去了Fred Chow讲义中的小例子)

      • 寄存器提升(寄存器变量识别)
        • 通过提升变量到伪寄存器(TNs in CG)来识别寄存器分配后备。即无数量限制的寄存器分配
        • 对于无别名的本地变量无意义(不需要取地址),本地的RVI(寄存器变量识别)只需要在符号表上工作。
        • 对于其他类型变量,问题转化为对loadstore做优化
        • 寄存器提升就是:对loadPRE,再对storePRE,先做Load PRE,将为Store PRE创造更多机会。
        • 高效的寄存器提升还需要投机PRE(?),这种PRE适用于循环内的分支。

      (译者注:此处略去了Fred Chow讲义中的小例子)

      • 基于全局值编号的冗余

        • PRE仅仅识别词法上独立表达式之间的冗余

        • 能从非语法独立表达式中提取冗余

        • 全局值编号能识别计算相同值的非独立表达式

        • Open64GVN算法基于Taylor Simpson的论文

        • GVN之后,在相同值编号的表达式之间删除冗余

      • 基于值编号的完全冗余删除(VNFRE)

        • PRE中设计的表达式不计算相同俄值,PHI在流图汇合点将不同的值合并

        • 在计算相同值的表达式之间尽可能存在完全冗余

        • VNFRE消除具有相同GVN的表达式之间的完全冗余

        • SSAPRE算法特殊化就能用于完全冗余
      • 强度削弱

        • 归约表达式—归约变量的线性函数(?)

        • 强度削弱使用归约变量的递增代替归约表达式的计算

        • 反向循环规范化(?)—定义:归约表达式提升为归约变量过程中可能会出错

        • 处理方法:
          1. 先将PRE应用于归约表达式上,不管是否出错。
          2. PRE之后,通过更新存储归约表达式值的临时变量来修复
      • 线性函数删除测试
        • 在最终测试(?)中,使用强度削弱归约表达式代替归约变量的使用
        • 目的:使原规约变量废弃
        • 归约表达式是归约变量的线性函数
        • SSAPRE规约表达式过程中进行:
          • LFTR中的实例机会将使用SSA图中的COMP_OCCUR表示
          • 当归约表达式在比较点可用时,也许能删除
          • 根据将归约表达式转换成归约变量函数形式的特点来调整比较的r.h.s(?)
        • Main-OPT阶段的累计优化效果
          • 结合了以下优化效果:PRE、强度削弱、线性函数测试删除
      • Main-OPT阶段结果:
        1. 降低位域代码(opt_revise_ssa.cxx)
        2. 表达式PRE(opt_etable.cxx)
          • 强度削弱(opt_estr.cxx)
          • 代码提升(opt_ehoist.cxx)
          • 线性函数测试删除(opt_lftr2.cxx)
        1. 基于值编号的完全冗余删除(opt_vn.cxx)
        2. 死代码删除(opt_dce.cxx)
        3. 寄存器提升:
        4. 位域的死代码删除(opt_bdce.cxx)
          • 本地寄存器变量识别(opt_sym.cxx)
          • Load PRE (opt_ltable.cxx)–活跃区间收缩
          • Store PRE (opt_stable.cxx)
        1. 获得WHIRL(opt_htable_emit.cxx)



    open64课程–过程间分析优化(IPA)

    转载自:http://www.lingcc.com/2009/12/14/10295/

    此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://www.lingcc.com

    Fred Chow 原版讲义见最后一页

    Open64课程–过程间分析优化

    • IPA的角色

    唯一在程序间的优化操作。分析:收集整个程序的信息; 优化:在程序过程之间进行优化。IPA的整个优化效果取决于它之后的优化;IPA也为之后的优化阶段提供了跨文件的信息。

    • 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文件)

    • 主IPA阶段概览

    它使用了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)



    Open64课程—代码生成(CG)

    转载自:http://www.lingcc.com/2009/12/16/10317/

    此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://www.lingcc.com
    Fred Chow 原版讲义见最后一页

    代码生成

    • 目标机信息表(targ_info)

    该机制来自Cydrome,并进行了增强。将目标机指令集,ABI和调度信息参数化。通过表生成机制来编译和链接,生成的表是用于CG阶段的C++文件(?).这种机制能将优化算法和体系结构细节分开,而且再移植到新结构上时能最小化编译器的改动,因为机器相关的内容存放在机器相关的文件夹内。支持ISA子集。不同处理器的调度信息实现编译成独立的.so文件,并使用编译选项控制dlopen()使用哪个.so文件

    • 代码生成中间表示(CGIR)

    这种中间表示的每个操作(op)对应一条机器指令,通过targ_info来格式化指令。在一个目标机op中操作数和结果都存放在TN中(或者寄存器符号中).TN有符号、直接量和寄存器三种类型。TN的类型都是依据指令格式制定的。每个操作都使用两个操作数,并写出一个结果(和RISC相似),某些特殊的指令也能写两个结果(如 mul)

    • 代码生成阶段结构

    • CG展开

    这部分的作用是将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

    • 跨迭代CSE(?)

    建立在跨迭代读冗余删除操作和减少寄存器压力操作的基础上

    • 指令调度

    在基本块范围内进行(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 给出

    • 通过基于优先级寄存器分配的GRA

    使用粗粒度的干扰方式,分配是以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,通过将该寄存器添加回可用集合的方式来释放;相同的寄存器可以分配给同个指令中的结果和操作数,这种情况下,寄存器移动的指令可以直接删除.

    • 应对X86寄存器的古怪特性

    需要在LRA开始阶段为每个BB增加带额外拷贝的额外TN.为遵守两个操作数格式:
    TN100=TN101+TN102
    变为
    TN100 = TN101
    TN100 = TN100 +TN102
    为遵守8位寄存器子类:
    TN100 = sete ….
    变为
    TN101 = sete …
    TN100 = TN101
    TN101是8位寄存器子类,TN100没有这种约束。

    • 应对X86的特殊寄存器

    一些指令需要特定的寄存器,需要为rax,rcx和rdx创建单寄存器子类。引入涉及严格遵循寄存器子类的TN拷贝

    TN100 TN101 = mul32 TN200 TN201
    变为
    eax = TN200
    eax edx = mul32 eax TN201
    TN100 = eax
    TN101 = edx

    • LRA可能耗尽寄存器

    因为每个TN必须分配一个寄存器,当无可用的寄存器分配给TN时,就调用Fix_LRA_Blues()来释放额外的寄存器,或者尝试重新LRA这个BB。对于函数Fix_LRA_Blues(),k可以使用不同的策略。主要有以下三种策略:溢出一个已经通过GRA分配的寄存器;重新做指令调度来减小寄存器压力;溢出一个先前分配且超过其活跃范围的寄存器。

    • 应对X87中的寄存器栈

    x87仅在-m32下支持,x87栈寄存器视为普通寄存器文件建模,在最后的指令调度阶段,将X87寄存器转换为类似栈的操作(x8664/cg_convert_x87.cxx);在BB之间转换时,栈都被维持在相同的状态。需要插入fxch指令。如果寄存器栈不再活跃,就使用X87指令中的弹出版本(pop version)

    • 全局代码外提

    该过程在本地寄存器分配和最终指令调度阶段之间进行(gcm.cxx);计算每个基本块中的关键路径;通过移动指令来调整基本块的方式来寻找缩短关键路径的机会,如将开始指令移动到基本块的前驱,将结束指令移动到基本块的后继。

    • 应对汇编代码潜逃(asm())

    在WHIRL中,使用ASM_STMT表示一个汇编语句,ASM_STMT的输入输出维护着WHIRL的接口。在CG中,展开成汇编伪操作(whirl2ops.cxx).输入和输出的接口也转换为了TN。接下来TN通过GRA/LRA赋予真实的寄存器。在代码输出阶段,使用被赋予的寄存器替换汇编代码串中的操作数(cgemit.cxx,x8664/cgtarget.cxx)




    Open64课程-循环嵌套优化(LNO)

    转载自: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. 依赖测试的输出是依赖向量,每一维表示一个循环嵌套的层。

    • LNO实施的三类优化

    数据缓存转换(?),协助其他优化的转换,向量化和并行化

    • 用于数据缓存的LNO转换

    通过缓存分块实现,即将循环转换为工作在能装在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)

    • 循环分解和合并

    循环分解:使得循环交换,向量化和并行化成为可能,减少冲突失效。对于循环合并:减少循环开销,增加数据重用,增大循环规模。

    • 协助其他优化的LNO转换

    标量展开和数组展开:减少循环内依赖,使并行化成为可能;标量重命名:循环嵌套就能分别优化,减少寄存器分配时的约束;数组标量化:改进寄存器分配;此外还有提升混乱的循环边界、最外层循环展开(该过程可以形成更大的循环体,减少循环开销)、数组替换(前向和后向)、IF语句提升(通过重复匹配的迭代来代替循环)、循环反转换(即将循环内无关的条件判断移出)、聚合-分散(?)、提升不变数组引用到循环外、迭代内CSE
    (译者注:本段提到的几个优化,Fred Chow都有例子在ppt中,本人偷懒没有附上,请到本文最后一页的原版ppt中参阅第13-18页)

    • LNO并行化

    并行化包括三部分: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的额外考虑

    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

    • 有削弱的SIMD

    可能需要对每个计算单元复制累加器

    • 生成向量Intrinsic

    为了分离出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;

    • LNO阶段结构

    预优化 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)

    此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://www.lingcc.com
    Fred Chow 原版讲义见最后一页




    Open64课程–反馈指导优化(FDO)

    转载自:http://www.lingcc.com/2009/12/23/10416/

    支持反馈指导优化(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做测试

    • 值轮廓信息的利用

    为某些常用的运行时值调优

    1. 浮点乘 x*y 若多数y为1,则转化为 if(y==1.0) x else x*y
    2. 整数除 i/j 若j多为2^k,则转化为if(j==2^k) i<<k else i/y
    3. 非直接调用 (*p)() 若p多为函数foo(),则转化为if(p == &foo) foo() else (*p)()
    此文是Fred Chow在德拉华大学所讲open64课程讲义的翻译,转载请注明出处 http://www.lingcc.com
    Fred Chow 原版讲义见最后一页




    Open64课程–OpenMp和自动并行化

    转载自:http://www.lingcc.com/2009/12/25/10418/

    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 编译过程

    首先前段将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编译指示在这一步被删除.

    • 较早阶段的MP

    是指当应用在LNO相关的MP lowering时,默认便已是用较晚的MP,MP的lowering多应用在LNO之后,LNO在有MP编译指示时运行–MP编译指示的出现让LNO的转换更加保守.选项-OPT:early_mp=on作用在较早的MP上,LNO引用并行例程时,若无MP编译指示,则使更激进的优化成为可能,附加的调用可能会抑制一些优化,在-apo选项下并不起作用.




    你可能感兴趣的:(算法,优化,存储,扩展,fortran,编译器)