LLVM笔记(9) - 指令选择(一) 概述
本文最初是基于对新员工培训, 使其快速上手编译器后端代码而写的入门简介. 为方便阅读又根据模块细分为若干章, 内容以分析代码为主, 偶尔也会穿插一些理论扩展.
什么是指令选择
指令选择(instruction selection)是将中间语言转换成汇编或机器代码的过程. 如果仅为单一语言在单一目标上实现指令选择, 可以使用手工编码的方法. 否则通过使用自动代码生成器生成代码, 编译器开发人员只需负责修改不同目标的机器指令描述, 是更优选择.
指令选择需要考虑的问题
- 目标的寄存器, 寻址方式和指令体系结构
目标的指令集与体系结构会影响指令选择的方式. e.g. 在X86上给一个寄存器赋值32bit立即数地址只需要mov一条指令, 但像ARM与RISCV等架构只支持12bit的立即数加法, 需要更加复杂的方式实现同样的移动语义. 根据codesize model的设置, RISCV可以使用lui + addi两条指令实现(分别移动高20bit与低12bit), 或将地址保存到当前指令地址附近然后用addi + lw两条指令实现(计算常量所在地址并访问该地址). - 软件调用约定(calling convention)
指令选择必须考虑兼顾目标的ABI定义. - 中间语言的结构和特征
本质上中间语言对生成的代码的正确性没有影响, 然而中间语言的结构影响对自动代码生成算法的设计. - 转化为机器代码的方式
略.
常见的指令选择实现
常见的指令选择实现可以参见经典书籍Survey on Instruction Selection(这块内容以后有空单独再介绍).
- 宏展开(macro expension)
- 树覆盖(tree covering)
- 有向图覆盖(DAG covering)
LLVM当前的指令选择实现
LLVM在O0编译时使用名为FastISel的指令选择PASS, 在O2编译时使用名为SelectionDAG的指令选择PASS, 同时当前社区还在推进名为GlobalISel的指令选择PASS(当前仅在AArch64上支持), 希望能替代SelectionDAG. 我们将首先介绍SelectionDAG(如未特殊说明, 下文介绍均默认为SelectionDAG), 然后会简要介绍FastISel与GlobalISel, 并比较三者特点, 讨论为什么要使用GlobalISel替换SelectionDAG.
什么是SelectionDAG
SelectionDAG是一种trees-on-DAGs的有向图覆盖实现, 编译器开发人员通过编写指令的树匹配(tree pattern), 通过tablgen翻译成完整的树匹配代码交由代码生成器(matcher generator)处理, 后者采用贪婪的DAG-to-DAG策略将中端IR覆写为机器指令描述.
SelectionDAG流程简介
SelectionDAG由若干个优化组合而成, 其具体流程图如下所示, 其中红色节点表示数据(输入的中间语言)的组成形式, 黑色节点表示处理模块.
我们会依据SelectionDAG的流程依次分析每个步骤的目标, 实现方式以及在支持一个新架构时需要修改的注意点, 这里先简要介绍各个模块的作用:
- 上文提到SelectionDAG是基于DAG covering的指令选择实现, 因此需要首先将输入程序流的表示方式从IR转换为DAG, 该过程又被称做lowering. lowering会将IR节点一一的翻译为DAG节点的同时也会处理调用约定, 使其遵守特定目标的ABI规范(这也是lowering名字的含义). 在lowering后程序流是以名为SDNode的节点组成的DAG.
- 根据特定目标所支持的指令集与寄存器结构, SelectionDAG将目标不支持的操作与数据类型转换为合法的操作与数据类型, 这一步被称作legalize. 另一方面对于其中产生的可以合并的操作被称作combine. 可以看到legalize与combine执行了多次, 且legalize每次执行的内容都不一样.
- 在combine和legalize之后是select步骤, 通过pattern match或手写代码的方式生成对应指令. 经过指令选择后的程序流是以Machine Instruction为节点组成的DAG.
- 在早期的SelectionDAG实现里还考虑的了调度的优化, 然而现在LLVM已经支持了PreRA与PostRA的调度, 在此处调度优化并不重要, 本文暂不涉及, 关于调度的实现将在以后调度器实现分析中介绍.
- 指令选择(步骤3)只是选出了对应的指令, 还需要将其重新覆写为基于SSA格式的Machine Instruction描述, 为每条指令分配虚拟寄存器, 连接PHI节点.