后端
由一套分析和转换趟
组成,任务是生成代码
,即把LLVM
中间(IR)
转换为目标代码(或汇编)
.
LLVM
支持广泛目标:ARM,AArch64,Hexagon,MSP430,MIPS,NvidiaPTX,PowerPC,R600,SPARC,SystemZ,X86,
和XCore
.
所有这些后端
共享一套,按通用API
方法抽象后端任务
的目标无关
生成代码的一部分,即公共接口
.
每个目标
必须特化
生成代码通用类
,以实现目标相关
行为.
这里,介绍LLVM
后端的一般性质
,对想编写新的后端
,维护已有后端
,或编写后端趟
,都是很有用
的.
要经历多个步骤
,才能转换LLVMIR
为目标汇编代码
.把IR
转换为后端友好的指令,函数,全局变量
的表示.随着程序经历各种后端变换
,该表示
越来越接近实际目标指令
.
下面简略描述生成代码
的各个阶段
:
1,选指(InstructionSelection)
过程,表示把内存
中的IR
转换为目标相关
的SelectionDAG
节点.
起初,该过程把三地址
结构的LLVMIR
转换为DAG
有向无环图形式.
每个DAG
可表示单个基本块的计算
,即每个基本块
关联不同的DAG
.典型节点
表示指令
,而边
代表它们的数据流依赖
等.
可转换为DAG
很重要,这样LLVM
生成代码库,可运用基于树
的模式
匹配选指算法
,经过一些调整
,也能在DAG
(而不仅是树
)上工作.
该过程
结束时,DAG
已将它所有的LLVMIR
节点转换为表示机器指令
而不是LLVM
指令的目标机器节点
.
2,选指之后,就知道了,会使用哪些目标指令
来计算每个基本块
.
在SelectionDAG
类中编码
.然而,需要返回三地址
表示,来决定基本块
内部的指令顺序
,因为DAG
并不表明相互独立指令
间的顺序
.
第1个
调度指令(InstructionScheduling)
,也叫前分配
寄存器(RA)
调度,排序
指令来尽量指令级并行
.
然后按MachineInstr
三地址表示转换
这些指令.
3,在分配
寄存器(RegisterAllocation)
前LLVMIR
的寄存器集是无限
的,它把无限引用
的虚寄存器
转换为有限
的目标相关
的寄存器集
,不够时挤入(spill)
到内存.
4,第2次
调度指令,也叫后分配
寄存器(RA)
调度,此时.因此时
在该点可获得真实
寄存器信息,某些类型
寄存器有额外风险和延迟
,可用它来改进指令顺序
.
5,发射代码(CodeEmission)
阶段表示把指令
从MachineInstr
转换为MCInst
实例.该新表示
更适合汇编器和链接器
,它有两个选择
:输出汇编代码
或输出(blob)
二进制块到指定目标代码格式
.
如此,整个后端流程
用到了四种不同层次
的指令表示
:内存中的LLVMIR
,SelectionDAG
节点,MachineInstr
,和MCInst
.
llc
是后端
主要工具.如果用前面的sum.bc
位码,可用下面
命令生成它的汇编代码
:
$ llc sum.bc -o sum.s
或可用下面命令,生成目标代码
:
$ llc sum.bc -filetype=obj -o sum.o
使用以上命令时,llc
会试选择匹配sum.bc
位码中指定目标三元组
的一个后端
.使用-march
选项可覆盖
它而选择
指定后端.如,用下面命令生成MIPS
目标代码:
$ llc -march=mips -filetype=obj sum.bc -o sum.o
如果运行llc-version
命令,llc
会显示所支持的-march
选项的完整列表.注意,该列表
和LLVM
配置中用到的-enable-targets
选项兼容.
注意,刚才确保让llc
用不同后端
为起初是为x86
编译的位码
生成代码.
因为C/C++
语言有目标相关
属性,所以相关性会在LLVMIR
中体现.
因此,当位码
目标三元组
和运行llc -march
的目标不匹配
时,必须小心.可能导致ABI
不匹配,坏程序行为
,甚至导致生成
代码失败.
然而一般,会成功生成代码
,但更糟糕,它有微妙
的bug
.
为了理解IR
的目标依赖.考虑程序分配了存储
不同串的char
指针的向量
,你用通用的C语句
malloc(sizeof(char*)*n)
来为串向量
分配内存.
如果给前端
指定了目标,比如32
位MIPS
架构,它生成的代码会让malloc
分配nx4
字节的内存,因为在32
位MIPS
上每个指针是4字节
.
然而,如果用llc
编译该位码
,并确保指定x86_64
架构,它就生成了个坏程序
.
运行时,会有潜在的段错误,因为x86_64
架构的每个指针是8字节
,这使得malloc
分配的内存
不够.
在x86_64
上正确的malloc
调用是分配nx8
字节.
在LLVM
源码树的不同目录
分散有后端实现
.在lib
目录和它的CodeGen,MC,TableGen
,和Target
子目录中,是生成代码
背后的主要库:
1,CodeGen
目录包含实现了所有通用
生成代码算法
的文件和头文件
:选指,调度指令,分配寄存器,和需要的分析
.
2,MC
目录实现了汇编器(汇编解析器)
,松弛算法
(反汇编器
),和特定目标文件格式
,如ELF,COFF,Macho
等等低级功能
.
3,TableGen
目录包含TableGen
工具,根据.td
文件中的高级
目标描述生成C++
代码的完整实现
.
4,在Target
的子目录
中实现各个目标,如包括多个.cpp,.h,
和.td
文件的Target/Mips
目录.为不同目标
实现类似功能
的文件
一般有相同名字
.
如果编写新的后端
,仅在Target
目录中的一个子目录
放置代码.如,用Sparc
来阐明Target/Sparc
子目录中的组织:
文件名 |
描述 |
---|---|
SparcInstrInfo.td,SparcInstrFormats.td |
指令和格式 定义 |
SparcRegisterInfo.td |
寄存器和寄存器类定义 |
SparcISelDAGToDAG.cpp |
择指 |
SparcISelLowering.cpp |
选择DAG 节点降级 |
SparcTargetMachine.cpp |
目标相关属性(如数据布局 和ABI )信息 |
Sparc.td |
定义计算机特征 ,CPU 变体和扩展 功能 |
SparcAsmPrinter.cpp |
发射 汇编代码 |
SparcCallingConv.td |
ABI 定义调用约定 |
一般,后端都按此代码结构
组织,因此开发者很容易地把一个后端
具体问题映射
到另一个后端
中.如,你正在编写Sparc
后端的SparcRegisterInfo.td
寄存器信息文件,
并且想知道x86
后端是如何实现
它的,只需要查看Target/X86
目录中的X86RegisterInfo.td
文件.
llc
的非共享
代码是相当小的(见tools/llc/llc.cpp
),如同其它LLVM
工具,大部分都按可重用的库
实现功能
.
对llc
,由生成代码库
提供它的功能
.这组库可分为目标相关
和目标无关
部分.在不同文件
中,保存生成目标相关
的库和目标无关
的库,这样可链接
期望的严格目标后端
.
如,配置LLVM
时设置-enable-targets=x86,arm
,这样llc
就只会链接x86
和ARM
的后端库
.
注意,所有的LLVM
库都以libLLVM
为前缀.在此
省略该前缀.下面列举了一些目标无关
生成代码的库:
1,AsmParser.a
:包含解析汇编文本
并实现汇编器
的代码
2,AsmPrinter.a
:包含打印汇编语言
,并实现生成汇编文件
后端的代码
3,CodeGen.a
:包含生成代码
算法
4,MC.a
:包含MCInst
类及其相关的类,并用来表示LLVM
允许的最低级
程序
5,MCDisassembler.a
:实现了一个读取
目标代码文件,并按MCInst
对象解码
字节的反汇编器
6,MCJIT.a
:实现(即时
)生成代码.
7,MCParser.a
:包含MCAsmParser
类的接口,用来解析
汇编文本,执行部分汇编器
工作
8,SelectionDAG.a
:包含SelectionDAG
及其相关的类
9,Target.a
:包含可让目标无关
算法请求其它库(目标相关部分
)实现的目标相关
功能的接口
另一方面,下面是目标相关
的库:
1,
:包含AsmParser
库的目标相关
的部分,负责为目标机器
实现汇编器
2,
:包含打印目标指令
的功能,并让后端生成汇编语言文件
3,
:包括具体寄存器处理规则
,选指和调度
等后端目标相关功能
的主体.
4,
:包含低级MC
设施的目标机器信息
,负责注册如MCCodeEmitter
等目标相关的MC
对象.
5,
:用目标相关
的功能补充了MCDisassembler
库,来建造可读字节
并解码它们为MCInst
目标指令的系统
.
6,
:负责在LLVM
生成代码系统中注册目标
,提供了让目标无关
的生成代码库可访问目标相关
功能的接口类
.
在这些库名字中,用目标名
替换
,如,X86AsmParser.a
是X86
后端的解析库
的名字.完整的LLVM
安装在
目录中包含
这些库.
LLVM
后端,如何用TableGen
LLVM
使用TableGen
面向记录的语言,来描述多个
编译器阶段
用到的信息.如,在前端中,简单讨论了如何用TableGen
文件(以.td
为扩展名)描述前端的不同诊断信息
.
最初,LLVM
团队开发TableGen
是为了帮助编写LLVM
后端的.尽管生成代码库设计
强调,要干净分离
不同目标特性
,
如,用不同的类
表示指令的寄存器信息和其他
,但是最终后端代码
,不得不在多个不同
文件中表示相同
的某种机器特征
.
问题是,不仅要编写后端代码
,还在代码中引入了信息冗余
,必须手工同步
.
如,想修改后端
如何处理寄存器,要修改代码
中几处不同部分:在分配
寄存器器中说明支持
的寄存器类型
;
在汇编打印器
中说明如何打印
该寄存器;在汇编解析器
中说明,按汇编如何解析
;及在反汇编器
中,说明它要知道的寄存器编码方式
.
这样,很难维护
后端代码.
为此,创造了关于目标的TableGen
中央信息库.
想法是:在单独位置
声明机器的某种特性
,如在
中描述机器指令
,然后TableGen
后端用该信息库
去具体实现,如生成自己写很烦的匹配模式选指
算法等任务.
如今,用TableGen
来描述各种
目标相关信息,如指令格式,指令,寄存器
,匹配模式DAG
,选指匹配顺序,调用惯例
,和目标CPU
属性(支持的指令集架构(ISA)
特征和处理器族)等.
注意,还在追求全自动
为处理器生成后端,模拟器,和硬件综合描述文件
.
典型方法
是用类似TableGen
的声明描述语言
表示所有机器信息
,然后用工具继承
要求的各种软件(和硬件)
,并求值,测试
处理器架构.
但这很难,和手写
工具相比,自动生成
工具质量很差.LLVMTableGen
是辅助
完成较小任务,但仍给你完整的控制权
,让你用C++
代码实现自定义逻辑
.
TableGen
语言由创建记录
的定义和类(class)
组成.def
定义用来根据class
和multiclass
关键字实例化
记录.
由TableGen
后端进一步处理
这些记录,如为以下组件生成领域相关
信息:生成代码,Clang
诊断,Clang
驱动选项,和静态解析器检查器
.
因此,由后端
给出记录
所表示的实际意思
,而记录
仅保存信息.
如,假设想为1个架构定义ADD
和SUB
指令,而ADD
有两种形式
:所有操作数
都是寄存器
,一个操作数是寄存器
一个是立即数
.
SUB
指令只有第1种
形式.看下面insns.td
文件的示例代码:
class Insn<bits <4> MajOpc, bit MinOpc> {
bits<32> insnEncoding;
let insnEncoding{15-12} = MajOpc;
let insnEncoding{11} = MinOpc;
}
multiclass RegAndImmInsn<bits <4> opcode> {
def rr : Insn<opcode, 0>;
def ri : Insn<opcode, 1>;
}
def SUB : Insn<0x00, 0>;
defm ADD : RegAndImmInsn<0x01>;
Insn
类表示一个普通指令
,RegAndImmInsn
表示另一种形式的指令.def SUB
定义了SUB
记录,而defm ADD
定义了两个记录:ADDrr
和ADDri
.
用llvm-tblgen
工具,可处理一个.td
文件并检查
结果记录:
$ llvm-tblgen -print-records insns.td
------------- Classes -----------------
class Insn<bits<4> Insn:MajOpc = {?,?,?,?}, bit Insn:MinOpc = ?> {
bits<5> insnEncoding = { Insn:MinOpc, Insn:MajOpc{0},
Insn:MajOpc{1}, Insn:MajOpc{2}, Insn:MajOpc{3} };
string NAME = ;
}
------------- Defs -----------------
def ADDri { //Insn ri
bits<5> insnEncoding = { 1, 1, 0, 0, 0 };
string NAME = "ADD";
}
def ADDrr { //Insn rr
bits<5> insnEncoding = { 0, 1, 0, 0, 0 };
string NAME = "ADD";
}
def SUB { //Insn
bits<5> insnEncoding = { 0, 0, 0, 0, 0 };
string NAME = ;
}
通过llvm-tblgen
工具还可使用TableGen
后端;输入llvm-tblgen -help
,会列举所有后端选项
.注意此例没有用LLVM
相关的域,且未与后端工作.
TableGen
更多信息
.td
文件如前,生成代码
广泛使用TableGen
记录来表达目标相关
信息.看看生成代码的TableGen
文件.
文件(如,X86.td
)定义了所支持的ISA
特性和处理器族.如,X86.td
定义了AVX2
扩展:
def FeatureAVX2 : SubtargetFeature<"avx2", "X86SSELevel", "AVX2", "Enable AVX2 instructions", [FeatureAVX]>;
def
关键字从SubtargetFeature类
类型定义
了FeatureAVX2
记录.最后参数
是在定义
中已包含的其它特性
的一个列表
.
因此,带AVX2
的处理器
包含所有AVX
指令.
此外,还可定义包含它提供的ISA
扩展和特性的处理器类型
:
def : ProcessorModel<"corei7-avx", SandyBridgeModel, [FeatureAVX, FeatureCMPXCHG16B, ..., FeaturePCLMUL]>;
文件还包含了所有其它的.td
文件,且是描述目标相关
域信息的主文件
.llvm-tblgen
工具必须总是从它那获得目标
的任意TableGen
记录.
如,用下面命令,输出x86
的一切记录:
$ cd <llvm_source>/lib/Target/X86
$ llvm-tblgen -print-records X86.td -I ../../../include
X86.td
文件有TableGen
用来生成X86GenSubtargetInfo.inc
文件的部分信息
,但不止,一般,不能从.td
文件映射
到.inc
文件.
为此,考虑
文件是个用TableGen
的include
指令包含
了所有其它的.td
文件的重要的顶层文件
.
因此,生成C++
代码时,TableGen
总是解析
所有后端.td
文件,使你可自由地在任意
的最合适位置放置
记录.
即使X86.td
包含了所有其它的后端.td
文件,除了include
指令,``内容也要同Subtargetx86
子目标定义保持一致
.
如果查看实现x86Subtarget
类的X86Subtarget.cpp
文件,会发现一个调用"#include"X86GenSubtargetInfo.inc"
的C++
预处理器指令,表明如何在普通的codebase
中嵌入TableGen
生成的C++
代码.
该特别的include
文件包含关联
了串描述及其它相关的资源特征的处理器特征常量
及处理器特性向量
.
在
文件中,定义寄存器和寄存器类
.之后在定义指令
中,寄存器类
把指令操作数
绑定到特定
寄存器集合中.
如,X86RegisterInfo.td
用下面语句
定义了16
位的寄存器:
let SubRegIndices = [sub_8bit, sub_8bit_hi], ... in {
def AX : X86Reg<"ax", 0, [AL,AH]>;
def DX : X86Reg<"dx", 2, [DL,DH]>;
def CX : X86Reg<"cx", 1, [CL,CH]>;
def BX : X86Reg<"bx", 3, [BL,BH]>;
...
此处let
构建指令,用来定义
额外的即{...}
区域中的所有记录
都有的SubRegIndices
字段.
从X86Reg
类继承16
位寄存器的定义,为每个
寄存器保存它的名字,数目
,及8位
子寄存器的列表
.如下重新产生16
位寄存器的寄存器类
定义:
def GR16 : RegisterClass<"X86", [i16], 16,
(add AX, CX, DX, ..., BX, BP, SP,
R8W, R9W, ..., R15W, R12W, R13W)>;
GR16
寄存器类,包含所有的16
位寄存器和它们各自分配寄存器
的优先顺序.在TableGen
处理后,每个寄存器类
的会得到RegClass
后缀,如,GR16
变成了GR16RegClass
.
TableGen
会生成寄存器和寄存器类
的定义,来收集它们的相关信息
,汇编器
的二进制编码
,和DWARF
(Linux
调试记录格式)信息.
可用llvm-tblgen
查看TableGen
生成的代码:
$ cd <llvm_source>/lib/Target/X86
$ llvm-tblgen -gen-register-info X86.td -I ../../../include
也可查看LLVM
编译过程中生成的C++
文件:
X86RegisterInfo.cpp
包含来辅助
定义X86RegisterInfo
类,inc
文件包含了寄存器的枚举
,调试后端
且不知道16
表示什么
寄存器时,它是一份有用的参考
.
分别在
和
文件中定义指令格式和指令
.指令格式
包含按二进制格式
写指令所必需的指令编码字段
,而指令记录
按单个记录
表示一条指令
.
可创建TableGen
类用来继承
指令记录的中间指令类
,以找出公共特征
,如相似数据处理
指令的公共编码
.
然而,每个指令或格式
必须是在include/llvm/Target/Target.td
中定义的指令
的TableGen
类的直接或间接
子类.
它的字段显示了在指令记录
中,TableGen
后端期望找到的内容
:
class Instruction {
dag OutOperandList;
dag InOperandList;
string AsmString = "";
list<dag> Pattern;
list<Register> Uses = [];
list<Register> Defs = [];
list<Predicate> Predicates = [];
bit isReturn = 0;
bit isBranch = 0;
...
dag
是个用来保存SelectionDAG
节点的特殊TableGen
类型.这些节点
表示选指过程
中的操作码,寄存器,或常量
.代码中这些字段
的意义:
1,OutOperandList
字段存储
结果节点,让后端
确定代表指令输出
的DAG
节点.
如,在MIPS
的ADD
指令中,按(outs GP32Opnd:$rd)
定义字段
.此例中:
1,outs
是个指示其子
是输出操作数
的特殊DAG
节点
2,GPR32Opnd
是MIPS
特有的指示MIPS32
位的通用
寄存器实例的DAG
节点
3,$rd
是用来识别节点
的任意寄存器名字
.
2,InOperandList
字段保存
输入节点,如,在MIPSADD
指令中,它是
(ins GPR32Opnd:$rs, GPR32Opnd:$rt)
3,AsmString
字段表示指令汇编串
,如,在MIPS
的ADD
指令中,它是"add$rd,$rs,$rt"
.
4,Pattern
是选择指令
时匹配
模式的dag
对象列表.如果匹配一个模式
,选指
会用该指令
替换匹配节点
.
如,在MIPS
的ADD
指令:
(set GPR32Opnd:$rd, (add GPR32Opnd:$rs, GPR32Opns:$rt))
模式中,[and]
表示只有一个在类似LISP
表示法的小括号
间定义的dag
元素列表的内容
.
5,Uses
和Defs
记录在执行指令
时,隐式使用和定义
的寄存器列表
.如,RISC
处理器的return
指令隐式使用返回地址寄存器
,而call
指令隐式定义返回地址寄存器
.
6,Predicates
字段,在选指
试匹配指令
前,存储
要检查的前提列表
.如果检查
失败了,就没有匹配
.如,一个前提
可能说明,该指令只对特定子目标
有效.
如果用选择了另一个子目标
的目标三元组
运行生成代码
,该前提
会求值
为假,而该指令
就不会匹配.
7,此外,其它还包括isReturn
和isBranch
字段,它们用指令行为信息
增强生成代码
.如,如果isBranch=1
,则生成代码
就知道该指令
是分支
指令,因此必须放在基本块尾
.
下面代码块中,可见在SparcInstrInfo.td
中的XNORrr
指令的定义.它用到了(在SparcInstrFormats.td
中定义的)F3_1
格式,它包括了SPARCV8
架构手册的F3
格式的一部分:
def XNORrr : F3_1<2, 0b000111,
(outs IntRegs:$dst), (ins IntRegs:$b, IntRegs:$c), "xnor $b, $c, $dst",
[(set i32:$dst, (not (xor i32:$b, i32:$c)))]>;
该XNORrr
指令有两个IntRegs
(一个表示SPARC32
位整数寄存器类
的目标相关
的DAG
节点)源操作数和一个IntRegs
结果,类似:
OutOperandList = (outs IntRegs:$dst)
InOperandList = (ins IntRegs:$b, IntRegs:$c)
AsmString
汇编通过$记号
引用指定的操作数
:"xnor $b,$c,$dst"
.模式
列表元素包含
应该匹配到该指令
的SelectionDAG
节点.
如,每当not
反转xor
的结果位,且xor
的两个操作数
都是寄存器
时,匹配XNORrr
指令.
为了查看XNORrr
指令记录字段,可用如下命令序列
:
$ cd <llvm_sources>/lib/Target/Sparc
$ llvm-tblgen -print-records Sparc.td -I ../../../include | grep XNORrr -A 10
多个TableGen
后端,用指令记录
信息干活,从相同指令记录
生成不同
的.inc
文件.这跟创建中心仓库
,用它给后端
各个部分生成代码
的TablenGen
的目标是一致的.
下面的每个文件
是由不同的TableGen
后端生成的:
1,
:用指令记录
中的模式
字段信息来发射选择SelectionDAG
数据结构的指令的代码
.在
文件中包含它.
2,
:包含在其它描述指令的表
中,列举目标
所有指令的枚举
.
在
,和
中包含.
然而,在包含TableGen
生成文件,改变如何在每个环境
中解析和使用
前,每个文件会定义一组特定宏
.
3,
:包含映射用来打印
每个指令汇编
的串的代码.在
文件中包含.
4,
:包含为每条指令
映射要输出的二进制代码
,从而生成机器代码
以填写目标文件
的函数.在
中包含.
5,
:实现可解码
字节序列并识别代表的目标指令
的表和算法
.用来实现反汇编工具
,在
文件中包含它.
6,
:实现目标指令
的汇编器的解析器
.在
文件中包含
了它两次,每次都有一组不同预处理宏
,从而改变解析
方式.
选指是转换LLVMIR
为代表目标指令
的SelectionDAG
节点(SDNode)
的过程.第一步
是根据LLVMIR
指令创建DAG
,创建带IR
操作节点
的SelectionDAG
对象.
接着,降级这些节点
后,组合DAG
,及标准化
等过程,使它更易匹配目标指令
.然后,选指
用节点匹配模式
方法来从DAG
到DAG
转换,转换SelectionDAG
节点为代表目标指令
的节点
.
注意,选指
是其中最耗时
的后端趟
.一项编译SPECCPU2006
基准测试的函数的研究表明,在LLVM3.0
中,以-O2
运行llc
工具,平均来说,选指趟
几乎花去一半
的时间.
SelectionDAG
类SelectionDAG
类,用DAG
表示每个基本块的计算
,每个SDNode
对应一个指令或操作数
.
DAG
的边通过use-def
关系确保操作
之间的顺序
.如果B
节点(如,add
)连接到A
节点(如,Constant<-10>
),即A节点
定义了一个值(32
位的-10
整数),而B节点
使用它(用作加法
).
因此,必须在B
前执行A
操作.黑色箭头
表示指示数据流依赖
的普通连线
,如add
示例.蓝色虚线
箭头表示确保两条指令
顺序的非数据流链
,否则它们是不相关的,如,load
和store
指令,如果访问相同内存位置
,则必须按原始程序顺序
.
前面图中,CopyToReg
操作,因为蓝色虚线
箭头,必须在X86ISD::RET_FLAG
之前.红色
连线保证
相邻节点必须结合
在一起,即必须紧挨
着执行,之间不能有其它指令
.
如,因为红色连线
,表明相同节点CopyToReg
和X86ISD::RET_FLAG
必须紧挨着调度.
根据它和它的用户
的关系,每个节点
可提供不同的值类型
.值
不必是具体
的,也可能是个(token)
抽象令牌.它可能有任意如下类型
:
1,节点
所提供的值可以是表示整数,浮点数,向量,或指针
等具体值类型
.从它的操作数
计算新值的数据处理
节点,就是一例.
类型
可以是i32,i64,f32,v2f32
(有两个f32
元素的向量),和iPTR
等.在LLVM
示意图中,当另一个
节点使用
该值时,由普通黑色连线
描绘生产者-消费者
关系.
2,Other
类型是表示链值
(ch
)的抽象令牌
.在LLVM
示意图中,另一
节点使用
一个Other
类型的值时,按蓝色虚线
打印连接
两者的连线
.
3,Glue
类型表示组合.在LLVM
示意图中,另一节点
使用Glue
类型值时,按红色
来画连接
两者的连线.
SelectionDAG
对象有个表示基本块入口
的EntryToken
的特殊令牌
,它通过消费首节点
,提供Other
类型的值,让链结
的节点以它为起点
.
SelectionDAG
对象也可引用
正好是按Other
类型的值链编码
关系的最后一条指令
的后续节点的图的根节点
.
在该阶段,可同时有目标无关
和目标相关
节点,这是执行预备步骤
的结果,如负责
准备选指DAG
的降级和合法化
.
然而,选指
结束时,所有目标指令
匹配的节点
都会是目标相关
的.前面,有如下目标无关
的节点:CopyToReg,CopyFromReg,Register(%vreg0),add,
和Constant
.
此外,如下为已预处理
且是目标相关
的节点(尽管选指后仍可改变
):TargetConstant,Register(%EAX)
,和X86ISD::REG_Flag
.
可观察到下面的语义:
1,Register
:可能引用虚或(目标相关的)物理
的寄存器.
2,CopyFromReg
:复制当前基本块域
外定义的寄存器,示例中,它复制
函数参数.
3,CopyToReg
:不提供其它节点
使用的具体值
的,复制值到指定寄存器
.
然而,该节点,要用不生成
具体值的其它节点
,产生一个要链接(Other
类型)的链值
.
如,为了使用写到EAX
的值,X86ISD::RET_FLAG
节点使用由Register(%EAX)
提供的i32
结果,且还消费由CopyToReg
产生的链,这样确保用CopyToReg
更新%EAX
,因为该链
会确保在X86ISD::RET_FLAG
前调度CopyToReg
.
SelectionDAG
细节,见llvm/include/llvm/CodeGen/SelectionDAG.h
头文件.对节点
结果类型,见llvm/include/llvm/CodeGen/ValueTypes.h
头文件.
llvm/include/llvm/CodeGen/ISDOpcodes.h
头文件定义
了目标无关
节点,而lib/Target/
头文件定义了目标相关
节点.
如果这是选指
输入,为何在SelectionDAG
中,已有一些目标相关
节点?
为此,首先在下图中给出选指前
步骤全局图
,在左上角从LLVMIR
步骤开始:
IR,1=>映射指令到SD节点,2=>SD节点=>组合1->标准化类型1
组合->合法化向量->合法化2=>组合=>合法化=>组合2=>选指
中间步骤中一堆降级.
首先,SelectionDAGBuilder
实例(见SelectionDAGISel.cpp
)访问每个函数
,为每个基本块
创建一个SelectionDAG
对象.
在此时,一些特殊的IR
指令如call
和ret
已要求目标相关
语句,如,如何传递调用参数
及如何从一个要转换为SelectionDAG
节点的函数
返回.
为此,第一次使用TargetLowering
类中的算法.该类
是每个目标
都必须实现的抽象接口
,且有大量所有后端
可使用的共享功能
.
为了实现该抽象接口
,每个目标
声明一个叫
的TargetLowering
的子类.每个目标还重载
实现具体的目标无关
高级节点应如何降级
到更接近的机器级
的方法.
如期,仅有小部分节点
必须这样降级
,而大部分在选指时就匹配和替换了其它节点
.如,在sum.bc
的SelectionDAG
中,用X86TargetLowering::LowerReturn()
方法(见lib/Target/X86/X86ISelLowering.cpp
)降级IR
的ret
指令.
同时,生成了复制函数结果
到EAX
的X86ISD::RET_FLAG
节点,按目标相关
方式处理函数
返回.
DAG
结合与合法化从SelectionDAGBuilder
输出的SelectionDAG
并不能直接选指
,必须经历附加
转换.在选指
前执行的趟
序列如下:
1,可获利时,DAG
结合趟
通过匹配
一系列节点
,并用简化
结构替换它们,来优化
次优化的SelectionDAG
结构.
如,可把(add(RegisterX),(constant0))
子图合并为(RegisterX)
.
类似,目标相关
组合方法可识别
节点模式,并根据是否可提高
此目标选择指令的质量
,决定是否合并
和折叠它们.
可在lib/CodeGen/SelectionDAG/DAGCombiner.cpp
文件中,找到LLVM
通用的DAG
结合的实现,在lib/Target/
文件中找到目标相关
的组合实现
.
setTargetDAGCombine()
方法,标记目标
想要结合的节点
.如,MIPS
后端试结合加法:见lib/Target/Mips/MipsISelLowering.cpp
中的setTargetDAGCombine(ISD::ADD)
和performADDCombine()
.
注意,在每次合法化
后运行DAG
结合,来最小化SelectionDAG
冗余.而且,DAG
结合知道在趟
链的何处
运行,(如在合法化类型
或向量
后),可运用
这些信息以更精确.
2,类型合法化趟
,确保选指
只需要处理目标天然支持
的类型的合法类型
.如,在只支持i32
类型的目标
上,i64
操作数的加法
是非法
的.
此时,合法化类型
,展开整数
把i64
操作数分为两个i32
操作数,同时生成合适节点
以操作它们.目标定义
了每种类型
所关联的寄存器
,并显式
声明了支持
类型.
这样,必须检测
并相应处理非法类型
:可提升,展开,或软化
标量类型,而可分解,标量化,或放宽
向量类型,见llvm/include/llvm/Target/TargetLowering. h
对每种情况的解释.
此外,目标还可设置
自定义方法来合法化
类型.两次运行
合法化类型
,第一次组合DAG
后,及在合法化向量
后.
3,有时,后端
直接支持向量类型
,即有个寄存器类
,但是没有处理给定向量类型
的具体操作
.如,x86
的SSE2
支持v4i32
向量类型.
然而,并没有x86
指令支持v4i32
类型的ISD::OR
操作,而只有v2i64
的.因此,向量合法化
会为指令
用合法类型
来处理,提升或扩展
.
目标
还可自定义
合法化.对前面提到的ISD::OR
,会提升操作
而使用v2i64
类型.看一看下面的lib/Target/X86/X86ISelLowering.cpp
的代码片:
setOperationAction(ISD::OR, v4i32, Promote);
AddPromotedToType (ISD::OR, v4i32, MVT::v2i64);
DAG
合法化类似向量
合法化,但是它用不支持
类型(标量或向量
)处理剩余
的操作.它支持相同动作
:提升,扩展,和自定义
节点.
如,x86
不支持以下三种
:i8
类型的有符号整数
到浮点
的转化操作(ISD::SINT_TO_FP)
,再请求合法化
提升操作;
32
位操作数的有符号除法
,(ISD::SDIV)
,发起一个产生库调用
处理该除法的扩展请求
;f32
操作数的浮点数绝对值
,用自定义
处理器生成有相同效果
的等价代码
.
x86
以如下发起这些动作(见lib/Target/X86/X86ISelLowering.cpp
):
setOperationAction(ISD::SINT_TO_FP, MVT::i8, Promote);
setOperationAction(ISD::SDIV, MVT::i32, Expand);
setOperationAction(ISD::FABS, MVT::f32, Custom);
DAG
到DAG
的选指DAG
到DAG
选指目的,是用匹配模式
转换目标无关
节点为目标相关
节点.选指
算法是本地的,每次在SelectionDAG
(基本块)的实例
上工作.
如,后面给出了选指
后最终的SelectionDAG
结构.直到分配
寄存器,CopyToReg,CopyFromReg
,和Register
节点不变.
选指过程
甚至可能增加节点
.选指之后,按X86
指令ADD32ri8
转换ISD::ADD
节点,而X86ISD::RET_FLAG
变为RET
.
注意,在同一个DAG
中并存有三个指令表示类型
:通用的LLVMISD
节点比如ISD::ADD
,目标相关的
节点比如X86ISD::REG_FLAG
,目标物理指令
比如X86::ADD32ri8
.