匹配模式
每个目标都有个叫<Target_Name>DAGToDAGISel
的SelectionDAGISel
子类.它通过实现子类
的Select
方法来选指
.
如SPARC
中的SparcDAGToDAGISel::Select()
(见lib/Target/Sparc/SparcISelDAGToDAG.cpp
文件).接收要匹配的SDNode
参数,返回代表物理指令
的SDNode
值;否则出错.
Select()
方法允许两种方式
来匹配物理指令
.最直接方式是调用
从TableGen
模式产生的匹配代码
,如下面列表
中的步骤一
.
然而,模式
可能表达不够清楚,就不能处理一些指令的奇怪行为
.此时,必须在``中实现自定义的C++
匹配逻辑,如下面列表
中的步骤二
.
下面详细介绍这两个方式:
1,Select()
方法调用SelectCode()
.TableGen
为每个目标
生成SelectCode()
方法,在此代码中,TableGen
还生成映射ISD
和<Target>ISD
为物理指令
节点的MatcherTable
.
该匹配器表是从.td
文件(一般为<Target>InstrInfo.td
)中的指令定义
生成的.SelectCode()
方法以调用目标无关
的,用目标
匹配器表匹配
节点的SelectCodeCommon()
结束.
TableGen
有一个专门的选指后端
,用来生成这些方法和表
:
$ cd <llvm_source>/lib/Target/Sparc
$ llvm-tblgen -gen-dag-isel Sparc.td -I ../../../include
对每个目标,在生成<build_dir>/lib/Target/<Target>/<Target>GenDAGISel.inc
名字的C++
文件中有相同输出;
如,对SPARC
中,可在<build_dir>/lib/Target/Sparc/SparcGenDAGISel.inc
文件中获得这些方法和表
.
2,在SelectCode
调用前,Select()
方法中提供自定义
匹配代码.如,i32
节点的ISD::MULHU
执行两个i32
的乘,产生一个i64
结果,并返回高i32
部分.
在32
位SPARC
上,SP::UMULrr
乘法指令,在要求SP::RDY
指令读它的Y特殊寄存器
中返回高位部分
.TableGen
无法表达该逻辑
,但是可用下面代码
解决:
case ISD::MULHU: {SDValue MulLHS = N->getOperand(0);SDValue MulRHS = N->getOperand(1);SDNode *Mul = CurDAG->getMachineNode(SP::UMULrr, dl, MVT::i32, MVT::Glue, MulLHS, MulRHS);return CurDAG->SelectNodeTo(N, SP::RDY, MVT::i32, SDValue(Mul, 1));
}
这里,N
是待匹配的SDNode
参数,这里,N
等于ISD::MULHU
.因为在该case
语句前,已仔细检查,这里生成SPARC
相关的操作码
来替换ISD::MULHU
.
为此,调用CurDAG->getMachineNode()
用SP::UMULrr
物理指令创建
节点.
接着,用CurDAG->SelectNodeTo()
,创建一个SP::RDY
指令节点,并将指向ISD::MULHU
的结果的所有use
(引用)改变
为指向SP::RDY
的结果.
前面的C++
代码片是lib/Target/Sparc/SparcISelDAGToDAG.cpp
中的代码的简化版本.
可视化选指过程
多个llc
的选项,可在不同的选指过程
可视化SelectionDAG
.如果使用了任意一个这些选项
,llc
类似前面,生成一个.dot
图,但是要用dot
程序来显示它,或用dotty
编辑它.可在www.graphviz.org
的Graphviz
包中找到它们.
下图按执行
顺序列举
了每个选项
:
llc选项 | 过程 |
---|---|
-view-dag-combine1-dags | DAG 合并1之前 |
-view-legalize-types-dags | 合法化类型 前 |
-view-dag-combine-lt-dags | 合法化类型 2后,组合DAG 前 |
-view-legalize-dags | 合法化前 |
-view-dag-combine2-dags | 组合DAG 2前 |
-view-isel-dags | 选指 前 |
-view-sched-dags | 选指 后,调度 指令前 |
快速选指
LLVM
还支持可选的快速选指
(FastISelclass
,在<llvm_source>/lib/CodeGen/SelectionDAG/FastISel.cpp
文件).
快速选指
目标是以损失代码质量
为代价,快速生成代码
,它适合-O0
优化级编译.通过省略复杂的合并和降级逻辑
,提速编译
.
对简单
操作,可用TableGen
描述,但更复杂的指令匹配
需要目标相关
代码来处理.
注意,-O0
管线编译还用到了快速但次优化
的分配寄存器器和调度器
,以代码质量
换取编译速度
.
调度
选指之后,SelectionDAG
结构有代表处理器
直接支持它们的物理指令
的节点
.下个阶段
是在SelectionDAG
节点(SDNode)
上工作的前分配寄存器调度器
.
有几个不同的待选调度器
,它们都是ScheduleDAGSDNodes
的子类(见<llvm_source>/lib/CodeGen/SelectionDAG/ScheduleDAGSDNodes.cpp
文件).
在llc
工具中,可通过-pre-RA-sched=<scheduler>
选项选择调度器类型
.可能的<scheduler>
值如下:
1,list-ilp,list-hybrid,source
,和list-burr
:这些选项指定由ScheduleDAGRRListclass
实现(见<llvm_source>/lib/CodeGen/SelectionDAG/ScheduleDAGRRList.cpp
文件)的列表调度算法
.
2,fast
:ScheduleDAGFastclass(<llvm_source>/lib/CodeGen/SelectionDAG/ScheduleDAGFast.cpp)
实现了个次优但快速
的调度器.
3,view-td
:一个由ScheduleDAGVLIWclass
实现(见文件<llvm_source>/lib/CodeGen/SelectionDAG/ScheduleDAGVLIW.cpp
)的VLIW
相关的调度器.
default
选项为目标
选择预定义的最佳调度器
,而linearize
选项不调度
.可获得的调度器
可使用指令行程表
和风险识别器
信息,来更好
调度指令.
注意,生成代码中有三个
不同调度器:两个在分配
寄存器之前,一个在分配
寄存器后.第一个在SelectionDAG
节点上工作,而其它两个在机器指令
上工作.
指令行程表
有些目标
提供了表示指令延迟
和硬件管线信息
的指令行程表
.在调度
策略时,调度器
用这些属性来最大化吞吐量,避免性能受损
.
在每个目标目录
中的一般叫<Target>Schedule.td
(如X86Schedule.td
)的TableGen
文件中保存这些信息
.
如下在<llvm_source>/include/llvm/Target/TargetItinerary.td
,LLVM
提供了ProcessorItineraries
的TableGen
类:
class ProcessorItineraries<list<FuncUnit> fu, list<ByPass> bp,
list<InstrItinData> iid> {...
}
目标
可能为一个芯片
或处理器族
定义处理器行程表
.要描述它们,目标必须提供(FuncUnit)
函数单元列表,管线支路(ByPass)
,和(InstrItinData
)指令行程数据.
如,如下,在<llvm_source>/lib/Target/ARM/ARMScheduleA8.td
中,为ARMCortexA8
指令的行程表
.
def CortexA8Itineraries : ProcessorItineraries<[A8_Pipe0, A8_Pipe1, A8_LSPipe, A8_NPipe, A8_NLSPipe],[], [...InstrItinData<IIC_iALUi ,[InstrStage<1, [A8_Pipe0, A8_Pipe1]>], [2, 2]>,...
]>;
这里,没有看到(ByPass)
支路.但看到了该处理器
的(A8_Pipe0,A8_Pipe1等)
函数单元列表,及IIC_iALUi
类型的指令行程数据
.
该类型
是形如reg=reg+immediate
的二元运算指令
的类
,如ADDri
和SUBri
指令.
如InstrStage<1,[A8_Pipe0,A8_Pipe1]
定义的那样
,这些指令花一个机器时钟周期
,以完成A8_Pipe0
和A8_Pipe1
函数单元.
接着,[2,2]
列表表示发射指令
后每个操作数读取或定义
所用的时钟周期
.此处,(index0)
目标寄存器和(index1)
源寄存器都在2个
时钟周期后可用.
检测风险
风险识别器
用处理器指令行程表
信息计算风险.ScheduleHazardRecognizer
类为风险识别器
实现提供了接口
,而它
的子类
实现了LLVM
默认的记分风险识别器
(见<llvm_source>/lib/CodeGen/ScoreboardHazardRecognizer.cpp
文件).
允许目标提供自己的识别器
.这是必需
的,因为TableGen
可能无法表达
具体约束,这时必须提供自定义实现
.
如,ARM
和PowerPC
都提供了ScoreboardHazardRecognizer
子类.
调度单元
在分配
寄存器前后,运行调度器
.然而,只有前者可使用SDNode
指令表示,而后者使用MachineInstr
类.
为了兼顾SDNode
和MachineInstr
,SUnit
类(见文件<llvm_source>/include/llvm/CodeGen/ScheduleDAG.h
)调度指令
时按单元
抽象了底层指令表示
.llc
工具可用-view-sunit-dags
选项输出调度单元
.
机器指令
在在<llvm_source>/include/llvm/CodeGen/MachineInstr.h
定义的MachineInstr
类(简称MI
)给出的指令表示
上分配寄存器.
在调度
指令后,会运行转换SDNode
格式为MachineInstr
格式的InstrEmitter
趟.如名,该表示比IR
指令更接近实际
目标指令.
与SDNode
格式及其DAG
形式不同,MI
格式是程序的三地址表示
,即为指令序列
而不是DAG
,这让编译器可高效
地表达具体调度决定
,即决定每个指令
的顺序.
每个MI
有一个(opcode)
操作码数字
和几个操作数
,操作码只对具体后端
有意义.
用llc
的-print-machineinstrs
选项,可输出所有注册的趟
之后的机器指令
,或用-print-machineinstrs=<趟名>
选项输出指定趟
后的机器指令
.
从LLVM
源码中查找这些趟
的名字.为此,进入LLVM
源码目录,运行grep
查找趟
注册名字时常用到的宏:
$ grep -r INITIALIZE_PASS_BEGIN * CodeGen/
PHIElimination.cpp:INITIALIZE_PASS_BEGIN(PHIElimination, "phi-node-elimination"
(...)
如,看下面sum.bc
的每个趟
之后的SPARC
机器指令:
$ llc -march=sparc -print-machineinstrs sum.bc
Function Live Ins: %I0 in %vreg0, %I1 in %vreg1
BB#0: derived from LLVM BB %entry Live Ins: %I0 %I1
%vreg1<def> = COPY %I1; IntRegs: %vreg1
%vreg0<def> = COPY %I0; IntRegs: %vreg0
%vreg2<def> = ADDrr %vreg1, %vreg0; IntRegs: %vreg2, %vreg1, %vreg0
%I0<def> = COPY %vreg2; IntRegs: %vreg2
RETL 8, %I0<imp-use>
MI
包含指令的重要元信息
:它存储使用和定义
的寄存器
,区分寄存器和内存
操作数(及其它类型
),存储
指令类型(分支,返回,调用,结束等
),预测运算是否可交换
,等等.
在像MI
此种低级层次,保存
这些信息很重要,因为在InstrEmitter
后,发射代码
前,运行的趟
,要根据这些字段
来分析
.
分配寄存器
分配寄存器
基本任务是,转换无限数量
的虚寄存器
为有限的物理寄存器
.因为目标的物理寄存器
数量有限,会把有些虚寄存器
调度到内存位置
,即spill槽
.
然而,甚至在分配
寄存器前,有些MI
代码可能已用到了物理寄存器
.当机器指令
要写结果
到指定的寄存器
,或ABI
要求时,就会这样
.
对此,分配
寄存器器承认
先前分配行为
,在此基础上,分配其余
物理寄存器给剩余
虚寄存器.
LLVM
分配寄存器器的另一个重要任务
是解构IR
的SSA
形式.直到此时,机器指令
可能还包含从原始LLVMIR
复制而来,且为了支持SSA
形式而必需的phi
指令.
如此,可方便地在SSA
之上,实现机器相关
优化.然而,传统的转换phi
指令为普通指令
方法,是用复制
指令替换它们.
这样,因为会赋值
寄存器并消除冗余
的复制
操作,不能在分配
寄存器后解构SSA
.
LLVM
有四种可在llc
中通过-regalloc=<regalloc_name>
选项选择的分配
寄存器方法.
可选的<regalloc_name>
有:pbqp,greedy,basic
,和fast
.
1,pbqp
:按分区布尔二次规划
问题映射
分配寄存器.一个PBQP
解决方法用来把该问题的结果
映射回寄存器
.
2,greedy
:高效全局(函数级
)分配
寄存器实现,支持活区间
分割及最小化挤出(spill)
.生动的解释.
3,basic
:提供扩展接口
的简单分配器
.
因此,它为开发
新的分配
寄存器器提供
基础,用作分配
寄存器效率的基线
.
4,fast
:该分配器
是局部
的(每基本块
级),它尽量在寄存器
中保存
值并重用
它们.
把默认
分配器映射为四种方法
之一,根据当前优化级(-O
选项)来选择.
虽然不管选择何种算法
,都在单一趟
中实现分配器,但是它仍依赖
组成分配器框架
的其它分析
.
分配器框架
用到一些趟
,这里介绍寄存器合并器
和寄存器覆盖
.
寄存器合并器
寄存器合并器(coalescer)
通过结合中间值(interval)
去除冗余的(COPY)
复制指令.RegisterCoalescer
类实现了为机器函数趟
的该合并(见lib/CodeGen/RegisterCoalescer.cpp
).
机器函数趟
类似IR趟
,它在每个函数
之上运行
,只是处理的不是IR
指令,而是MachineInstr
指令.
合并时,joinAllIntervals()
遍历复制
指令列表.joinCopy()
方法从机器
复制指令创建CoalescerPair
实例,并在可能
时合并掉
复制指令.
中间值(interval)
表示程序中的从产生时开始,最终使用为结束的开始和结束
的一对点
.
看看,在sum.bc位码
示例上运行合并器
会怎样.
用llc
中的regalloc
调试选项来查看合并器
的调试输出:
$ llc -march=sparc -debug-only=regalloc sum.bc 2>&1 | head -n30
Computing live-in reg-units in ABI blocks.
0B BB#0 I0#0 I1#0
********* INTERVALS **********
I0 [0B,32r:0) [112r,128r:1) 0@0B-phi 1@112r
I1 [0B,16r:0) 0@0B-phi
%vreg0 [32r,48r:0) 0@32r
%vreg1 [16r,96r:0) 0@16r
%vreg2 [80r,96r:0) 0@80r
%vreg3 [96r,112r:0) 0@96r
RegMasks:
********** MACHINEINSTRS **********
# Machine code for function sum: Post SSA
Frame Objects:
fi#0: size=4, align=4, at location[SP]
fi#1: size=4, align=4, at location[SP]
Function Live Ins: $I0 in $vreg0, $I1 in %vreg1
0B BB#0: derived from LLVM BB %entry
Live Ins: %I0 %I1
16B %vreg1<def> = COPY %I1<kill>; IntRegs:%vreg1
32B %vreg0<def> = COPY %I0<kill>; IntRegs:%vreg0
48B STri <fi#0>, 0, %vreg0<kill>; mem:ST4[%a.addr]
IntRegs:%vreg0
64B STri <fi#1>, 0, %vreg1; mem:ST4[%b.addr] IntRegs:$vreg1
80B %vreg2<def> = LDri <fi#0>, 0; mem:LD4[%a.addr]
IntRegs:%vreg2
96B %vreg3<def> = ADDrr %vreg2<kill>, %vreg1<kill>;
IntRegs:%vreg3,%vreg2,%vreg1
112B %I0<def> = COPY %vreg3<kill>; IntRegs:%vreg3
128B RETL 8, %I0<imp-use,kill>
# End machine code for function sum.
注意,可用-debug-only
选项,对指定的LLVM趟
或组件
开启内部调试消息
.为了找出要调试
的组件,可在LLVM
源码目录中运行grep -r "DEBUG_TYPE" *
.
DEBUG_TYPE
定义激活当前文件
调试消息的令牌选项
,如在分配
寄存器的实现文件
中有:
#define DEBUG_TYPE "regalloc"
注意,用2>&1
重定向了打印调试信息
的标准错误
输出到标准输出
.然后,用head-n30
重定向标准输出
(包含调试信息
),只打印前面的30
行.
这样,控制了显示
在终端上的信息量
.
先看看**MACHINEINSTRS**
输出.这打印了寄存器合并器
输入的所有机器指令
,如果在phi
节点消除趟
后,用-print-machine-insts=phi-node-elimination
选项输出(在合并器运行前的)机器指令
,也会得到相同的内容.
然而,合并器调试器的输出
,用索引信息
对每条(如0B,16B,32B
等)增强机器指令
.需要正确
解释中间值(interval)
.
这些索引也叫槽索引
,给每个活区间(liverange)
赋予不同数字.B字母
对应基本块(block)
,用于活区间
进入或离开基本块
边界.
此例中,用索引加B
打印指令,因为这是默认槽(槽)
.在中间值
中,有个不同表示寄存器
的r
字母槽,来指示普通寄存器
的使用或定义
.
阅读机器指令序列
,已知道了分配
寄存器器父趟
(多个小趟
的组合)的重要内容:%vreg0,%vreg1,%vreg2,%vreg3
都是虚寄存器
,要为它们分配物理寄存器
.
因此,除了%I0
和%I1
外,最多用4个
已在使用的物理寄存器
.按ABI
调用约定,要求在这些寄存器
中保存函数参数
.
因为在合并寄存器
前,运行活变量
分析趟
,代码也标注
了活变量信息
,表示在哪定义和干掉
每个寄存器
,这样可清楚哪些
寄存器相互冲突
,及可同时使用哪些
寄存器,并需要在不同物理寄存器
中保存.
另一方面,合并器
不依赖分配
寄存器器结果
,它只是查找寄存器
副本.
并从寄存器
到寄存器
的复制,合并器
会试用目标
寄存器的中间值
结合源寄存器
,与索引16
和32
的复制一样,让它们在相同
物理寄存器中,从而消除复制指令
.
接着是从合并
寄存器所依赖的另一个
分析的***INTERVALS***
消息:由lib/CodeGen/LiveIntervalAnalysis.cpp
实现的活中间值
分析(不同于活变量分析
).
合并器
要知道每个虚寄存器
所要活的中间值
,这样才能发现可合并哪些中间值
.如,可从输出
中看到,按[32r:48r:0)
确定%vreg0
虚寄存器的中间值
.
即,在32
处定义,在48
处干掉的该半开放
的%vreg0
中间值.48r
后面的0数字
是显示在哪第一次定义该中间值
的一个代码
,意思是刚好在中间值后面
打印:o:32r
.
这样,0定义就在索引32
位置.然而,如果中间值
分裂,这可有效
追踪原始定义
.
最后,RegMasks
显示了清理了冲突
源头的很多寄存器
的调用点
.
因为没有调用该函数
,所以没有RegMasks
位置.
观察中间值
,会发现:%I0
寄存器的中间值是[0B,32r:0)
,%vreg0
寄存器的中间值
是[32r,48r:0)
,在32
处,有一条复制%I0
到%vreg0
的复制指令
.
这是合并
前提:用[32r,48r:0)
组合中间值[0B,32r:0)
,并赋值给%I0
和%vreg0
相同寄存器
.
下面,打印其余调试输出
,看看:
$ llc -match=sparc -debug-only=regalloc sum.bc
...
entry:
16B %vreg1<def> = COPY %I1;
IntRegs: %vreg1考虑用%I1合并进%vreg1可合并至保留寄存器
32B %vreg0<def> = COPY %I0;
IntRegs:%vreg0考虑用%I0合并进%vreg0可合并至保留寄存器
64B %I0<def> = COPY %vreg2;
IntRegs:%vreg2考虑用%I0合并进%vreg2可合并至保留寄存器
...
看到,如期合并器
考虑用%I0
结合%vreg0
.然而,当有个如%I0
寄存器是物理
寄存器时,它实现了个特殊规则
.
必须保留
物理寄存器来合并它的中间值
.即,不能分配
物理寄存器给其它活区间
,而%I0
不是这样.
因此,合并器
放弃了,它担心过早地把%I0
赋值给这整个区间
,最后收益不大,并交给分配
寄存器器来决定.
因此,虽然结合虚寄存器
和函数参数寄存器
,sum.bc
程序没有合并
的机会,失败了,因为此阶段
它只能用保留
的虚寄存器,而非普通可分配
的物理
寄存器合并.
覆盖虚寄存器
分配
寄存器趟
为每个虚寄存器
选择物理寄存器
.随后,VirtRegMap
保存映射虚寄存器
到物理寄存器
的分配
寄存器结果.
接着,虚寄存器
覆盖由在<llvm_source>/lib/CodeGen/VirtRegMap.cpp
文件中实现的VirtRegRewriter
类表示的趟
,用VirtRegMap
并用物理
寄存器替换虚寄存器
.
根据情况会生成spill
代码.而且,会删除剩下
的reg=COPY reg
复制标识.
如,用-debug-only=regalloc
选项分析
分配器和重写器如何处理sum.bc
.首先,greedy
分配器输出如下文本:
...
assigning %vreg1 to %I1: I1
...
assigning %vreg0 to %I0: I0
...
assigning %vreg2 to %I0: I0
用%I1,%I0,%I0
物理寄存器分别分配1,0,2
虚寄存器.VirtRegMap
输出相同内容,如下:
[%vreg0 -> %I0] IntRegs
[%vreg1 -> %I1] IntRegs
[%vreg2 -> %I0] IntRegs
然后,重写器
用物理
寄存器替换
所有虚寄存器
,并删除复制标识
:
> %I1<def> = COPY %I1
删除复制标识
> %I0<def> = COPY %I0
删除复制标识
...
可见,尽管合并器
无法去除
这些复制,但是分配
寄存器器可如期为两个活区间
赋值相同寄存器
,并删除复制操作
.
最终,极大地简化了sum
结果函数的机器指令
:
0B BB#0: derived from LLVM BB
%entry
Live Ins: %I0 %I1
48B %I0<def> = ADDrr %I1<kill>, %I0<kill>
80B RETL 8, %I0<imp-use>
注意,删除了复制指令
,没有虚寄存器
.
注意,仅当LLVM
按debug
模式编译(配置时设置-disable-optimized
)后,才能使用llc
程序的-debug
或-debug-only=<name>
选项.
编译器中,分配
寄存器和调度指令
是天生的敌人
.分配
寄存器要求尽量让活区间
短一点,减少冲突图的边
的数目,以减少必要寄存器
的数目,以避免挤出(spill)
.
因而,分配
寄存器器喜欢串行
排列指令(让指令
紧跟在其依赖指令
的后面),这样代码所用的寄存器
就相对较少.
而调度指令
却相反:为了提升指令级的并行
,需要尽量地让很多无关而并行
的运算保持活动,要用很多
寄存器保存
中间值,增加活区间
之间的冲突数
.
设计有效算法来协同
调度指令和分配寄存器,是个开放
课题.
勾挂目标
合并时,要顺利
合并,虚寄存器
要来自兼容的寄存器类
.生成代码
从抽象方法
取得的目标相关
的描述中获得
这类信息.
分配器可从TargetRegisterInfo
的子类(如X86GenRegisterInfo
)获得寄存器
的包括是否为保留
,父寄存器分类
,是物理
还是虚
的所有寄存器信息
.
<Target>InstrInfo
类是另一个提供分配
寄存器器要求的目标相关
信息的数据结构,一些示例:
1,<Target>InstrInfo
的isLoadFromStackSlot()
和isStoreToStackSlot()
方法,用来挤出生成代码
时,判断机器指令
是否访问栈槽内存
.
2,此外,用storeRegToStackSlot()
和loadRegFromStackSlot()
方法,生成访问栈槽
的目标相关
的内存访问指令
.
3,在覆盖寄存器
后,可能保留COPY
指令,因为没有合并掉它们
,且不是同一个复制
.此时,copyPhysReg()
方法,必要时甚至在不同寄存器分类
间,用来生成目标相关
的寄存器复制
.
SparcInstrInfo::copyPhysReg()
中示例如下:
if (SP::IntRegsRegClass.contains(DestReg, SrcReg))BuildMI(MBB, I, DL, get(SP::ORrr), DestReg).addReg(SP::G0).addReg(SrcReg, getKillRegState(KillSrc));
...
生成代码
中,到处可见生成机器指令
的BuildMI()
方法.本例中,用SP::ORrr
指令来复制
一个CPU
寄存器到另一个CPU
寄存器.
片头和片尾
完整函数
都需要片头(prologue)
和片尾(epilogue)
.前者在函数
的开始处安装栈帧
及保存被调
的寄存器
,而后者在函数返回
前清理栈帧
.
在sum.bc
示例中,为SPARC
编译时,插入片头和片尾
后,机器指令如下:
%06<def> = SAVEri %06, -96
%I0<def> = ADDrr %I1<kill>, %I0<kill>
%G0<def> = RESTORErr %G0, %G0
RETL 8, %I0<imp-use>
此例中,SAVEri
指令是片头
,RESTORErr
是片尾
,来执行栈帧相关
的安装和清理
.片头和片尾
的生成是目标相关
的,由<Target>FrameLowering::emitPrologue()
和<Target>FrameLowering::emitEpilogue()
方法定义(见<llvm_source>/lib/Target/<Target>/<Target>FrameLowering.cpp
文件).
帧索引
在生成代码时,LLVM
用到一个虚栈帧
,用帧索引
引用栈元素
.插入片头
会分配
栈帧,给出充足的目标相关
信息,让生成代码
可用实际的(目标相关
)栈引用
替换索引虚帧
.
<Target>RegisterInfo
类的eliminateFrameIndex()
方法通过对包含栈引用
(一般为load
和store
)的所有机器指令
,把每个帧索引
转换为实际的栈偏移
,实现了上述替换
.
需要额外的栈偏移算术运算
时,也会生成额外指令
.见<llvm_source>/lib/Target/<Target>/<Target>RegisterInfo.cpp
文件.
理解机器代码框架
机器代码
(简称MC
)类包含整个低级操作函数和指令的框架
.对比其它后端组件
,这是新设计的帮助创建基于LLVM
的汇编器和反汇编器
的框架.
之前,LLVM
缺少一个整合
汇编器,编译过程只能到达创建汇编文本文件
,再生成汇编语言
这一步,要依靠(汇编器和链接器
)外部工具继续
剩余编译工作.
MC
指令
在MC
框架中,机器代码指令(MCInst)
替代了机器指令(MachineInstr)
.在<llvm_source>/include/llvm/MC/MCInst.h
文件中定义的MCInst
类,定义了指令的轻量表示
.
对比MI
(机器指令),MCInst
记录较少的程序信息
.如,不仅可由后端
,还可由反汇编器
根据二进制代码
创建MCInst
实例,注意反汇编器
缺少指令环境信息
.
事实上,它融入了汇编器
的理念,也即,目的不是丰富的优化
,而是组织
指令来生成目标文件
.
操作数
可以是一个寄存器
,立即数
(整数或浮点数
),式(表示为MCExpr
),或另一个MCInstr
实例.式来表示标签(label)
计算和重定位.在生成代码
阶段早期,就转换MI
指令为MCInst
实例.
发射代码
在后注册分配趟
后,发射
代码.从打印汇编(AsmPrinter)
开始生成
代码.
从MI
指令到MCInst
,接着到汇编或二进制
指令步骤:
1,AsmPrinter
是个,先生成函数头
,然后遍历所有基本块
,为进一步处理,每次发送一个MI
指令到EmitInstruction()
方法的机器函数趟
.
每个目标
会提供一个重载此方法的AsmPrinter
子类.
2,<Target>AsmPrinter::EmitInstruction()
方法按输入接收MI
指令,并用MCInstLowering
接口转换为MCInst
实例,每个目标
会提供该接口
的子类,并自定义生成
这些MCInst
实例的代码.
3,此时,可生成汇编或二进制
指令.MCStreamer
类通过两个子类
处理MCInst
指令流,并按所选格式
发射:MCAsmStreamer
和MCObjectStreamer
.
前者转换MCInst
为汇编语言
,而后者
转换为二进制指令
.
4,如果生成汇编指令
,就会调用MCAsmStreamer::EmitInstruction()
,并用目标相关
的MCInstPrinter
子类打印汇编指令
到文件.
5,如果生成二进制指令
,MCObjectStreamer::EmitInstruction()
的特殊目标(target)
和(object)
目光相关的版本
,就会调用LLVM
目标代码汇编器.
6,汇编器会用可从MCInst
实例分离的特化的MCCodeEmitter::EncodeInstruction()
方法,按目标相关
方式,编码和输出
二进制指令数据块
到文件.
此外,可用llc
工具输出MCInst
片段.如可用下面命令,编码MCInst
进汇编注释
:
$ llc sum.bc -march=x86-64 -show-mc-inst -o -
...
pushq %rbp ## <MCInst #2114 PUSH64r## <MCOperand Reg: 107>>
...
然而,如果想要按汇编注释
显示每条指令的二进制编码
,就用下面的命令:
$ llc sum.bc -march=x86-64 -show-mc-encoding -o -
...
push %rbp ## encoding: [0x55]
...
llvm-mc
工具,还可让你测试和使用MC
框架.如,为了找特定指令
的汇编编码
,使用-show-encoding
选项.下面是x86
指令的一例:
$ echo "movq 48879(,%riz), %rax" | llvm-mc -triple=x86_64 --show-encoding#encoding:
[0x48, 0x8b, 0x04, 0x25, 0xef, 0xbe, 0x00, 0x00]
该工具还提供了反汇编
功能,如下:
$ echo "0x8d 0x4c 0x24 0x04" | llvm-mc --disassemble -triple=x86_64leal 4(%rsp), %ecx
另外,-show-inst
选项为汇编或反汇编
指令,显示MCInst
指令:
$ echo "0x8d 0x4c 0x24 0x04" | llvm-mc --disassemble --show-inst -triple=x86_64leal 4(%rsp), %ecx # <MCInst #1105 LEA64_32r# <MCOperand Reg:46># <MCOperand Reg:115># <MCOperand Imm:1># <MCOperand Reg:0># <MCOperand Imm:4># <MCOperand Reg:0>>
MC
框架让LLVM
可为经典目标文件
阅读器提供可选择工具.如,目前默认编译LLVM
会安装llvm-objdump
和llvm-readobj
工具.
两者都用到了MC
反汇编库,实现了与GNUBinutils
包中的等价物(objdump
和readelf
)类似功能.
编写你自己的机器趟
展示如何编写
正好在生成代码
前的自定义机器趟
:统计每个函数
有多少机器指令
.不同于IR趟
,你不能用opt
工具运行该趟
,或通过命令行
加载并调度运行
.
由后端
代码管理机器趟
.因此,修改已有后端
来运行并观察
自定义趟
,这里选择SPARC
后端.
查找后端实现的TargetPassConfig
子类.如果用grep
,就会在SparcTargetMachine.cpp
中找到它:
$ cd <llvmsource>/lib/Target/Sparc
$ vim SparcTargetMachine.cpp
# 使用你喜欢的编辑器
查看从TargetPassConfig
继承的SparcPassConfig
类,看到它覆盖(override)
了addInstSelector()
和addPreEmitPass()
,但是如果想在其它
地方添加趟
,可覆盖其他很多方法.
见文档.在生成代码
前运行该趟
,因此在addPreEmitPass()
中添加代码:
bool SparcPassConfig::addPreEmitPass() {addPass(createSparcDelaySlotFillerPass(getSparcTargetMachine()));addPass(createMyCustomMachinePass());
}
上面代码中,高亮的行是额外添加
的,它调用函数createMyCustomMachinePass()
来添加该趟
.然而,还未定义该函数
.
用趟
代码及该函数写新的源码文件
.于是,创建叫MachineCountPass.cpp
的文件,填写下面内容:
#define DEBUG_TYPE "machinecount"
#include "Sparc.h"
#include "llvm/Pass.h"
#include "llvm/CodeGen/MachineBasicBlock.h"
#include "llvm/CodeGen/MachineFunction.h"
#include "llvm/CodeGen/MachineFunctionPass.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
namespace {class MachineCountPass : public MachineFunctionPass {public:static char ID;MachineCountPass() : MachineFunctionPass(ID) {}virtual bool runOnMachineFunction(MachineFunction &MF) {unsigned num_instr = 0;for (MachineFunction::const_iterator I = MF.begin(), E = MF.end(); I != E; ++I) {for (MachineBasicBlock::const_iterator BBI = I->begin(), BBE = I->end(); BBI != BBE; ++BBI) {++num_instr;}}errs() << "mcount --- " << MF.getName() << " has " << num_instr << " instructions.\n";return false;}};
}
FunctionPass *llvm::createMyCustomMachinePass() {return new MachineCountPass();
}
char MachineCountPass::ID = 0;
static RegisterPass<MachineCountPass> X("machinecount", "Machine Count Pass");
在第一行中,定义了DEBUG_TYPE
宏,这样以后就可通过-debug-only=machinecount
选项调试该Pass
.然而,本例中,未用到调试输出
.
剩余代码和前面为IRPass
写的类似.不同之处如下:
1,在包含文件中,包含了定义提取MachineFunction
信息及计数包含的机器指令
的类的MachineBasicBlock.h,MachineFunction.h,MachineFunctionPass.h
头文件.
因为声明createMyCustomMachinePass()
,还包含了Sparc.h
头文件.
2,创建了一个从MachineFunctionPass
而不是从FunctionPass
继承的类.
3,覆盖了runOnMachineFunction()
而不是runOnFunction()
方法.另外,方法
实现也是相当不同
的.遍历了当前MachineFunction
中的所有MachineBasicBlock
实例.
然后,对每个MachineBasicBlock
,调用begin()/end()
语句来计数所有机器指令
.
4,定义了createMyCustomMachinePass()
函数,在修改的SPARC
后端文件中按生成代码
前的趟
,创建和添加
该趟
.
既然已定义了createMyCustomMachinePass()
函数,就必须在头文件
中声明它.编辑Sparc.h
文件.在createSparcDelaySlotFillerPass()
后面添加函数声明
:
FunctionPass *createSparcISelDag(SparcTargetMachine &TM);
FunctionPass *createSparcDelaySlotFillerPass(TargetMachine &TM);
FunctionPass *createMyCustomMachinePass();
//最后1行.
下面用LLVM
编译系统编译
新的SPARC
后端.
如果已有了配置
项目的build
目录,进入
该目录,运行make
以编译新的后端.接着,可安装包含修改了的SPARC
后端的新的LLVM
,或依你所愿,只是从你的build
目录运行新的llc
二进制程序,而不运行makeinstall
:
$ cd <llvm-build>
$ make
$ Debug+Asserts/bin/llc -march=sparc sum.bc
mcount --- sum has 8 instructions.
如果想知道该趟
在趟
管道中的插入位置
,输入下面命令:
$ Debug+Asserts/lib/llc -march=sparc sum.bc -debug-Pass=Structure
(...)
Branch Probability Basic Block Placement
SPARC Delay Slot Filler
Machine Count Pass
MachineDominator Tree Construction
Sparc Assembly Printer
mcount --- sum has 8 instructions.
可看到,恰好在SPARCDelaySlotFiller
后,及生成代码
的SparcAssemblyPrinter
前调度
该趟
.
编写自己的后端.
后端设计.