1. 概述
指令选择(instruction selection,也称为代码选择,有时甚至称为代码生成)是编译器代码生成器后端所涉及的重要问题之一。另外两个重要问题是指令调度与寄存器分配。指令选择器负责通过尽可能地用好可用的机器指令,将程序从目标机器无关的表示翻译到一个目标机器特定的形式。这使得两个正交的子问题必须得到解决:
实际上Tablegen对后两个问题也提供了相当程度自动化的支持。不止于此,Tablegen还对LLVM的整体式(integrated)汇编器提供了强大的自动化支持。汇编码的解析、目标机器格式的编解码、汇编文件与目标文件的读写等功能的相当部分代码,都可由Tablegen根据TD描述文件自动生成。另外,Tablegen所基于的TD描述格式有高度的可扩展性,比如这篇博客所描述的对Hexagon目标机器所进行的指令关系框架的扩展。
LLVM的指令选择是基于DAG(directed acyclic graph,有向无环图)上的树模式匹配。其指令选择器生成的自动化程度很高。LLVM内部使用一个具有C++相似语法的高级语言对机器进行描述,而Tablegen解析这些描述,并生成指令选择器的大部分源代码(LLVM仍然需要定制部分源代码)。这些描述文件的后缀是“.td”(为了方便我们称之为TD语言),它们分布在llvm/lib/Target目录下面的各个特定体系架构目录。以X86为例,包括下列文件(来源:LLVM-3.6):
X86.td:对X86架构的描述。
X86CallingConv.td:X86架构的调用规范。
X86InstrInfo.td:X86架构的基本指令集。
X86InstrMMX.td:MMX指令集。
X86InstrMPX.td:MPX(Memory Protection Extensions)指令集。
X86InstrSGX.td:SGX(Software Gard Extensions)指令集。
X86InstrSSE.td:SSE指令集。
X86InstrSVM.td:AMD SVM(Secure Virutal Machine)指令集。
X86InstrTSX.td:TSX(Transactional Synchronziation Extensions)指令集。
X86InstrVMX.td:VMX(Virtual Machine Extensions)指令集。
X86InstrSystem.td:特权指令集。
X86InstrXOP.td:对扩展操作的描述。
X86InstrFMA.td:对融合乘加指令的描述。
X86InstrFormat.td:对X86指令格式定义的描述。
X86InstrFPStack.td:对X86浮点单元指令集的描述。
X86InstrExtension.td:对零及符号扩展的描述。
X86InstrFragmentsSIMD.td:描述SIMD所使用的模式片段。
X86InstrShiftRotate.td:对shift及rotate指令的描述。
X86Instr3DNow.td:对3DNow!指令集的描述。
X86InstrArithmetic.td:对X86架构的算术指令的描述。
X86InstrAVX512.td:对X86 AVX512指令集的描述(7.0大幅增强,从6387行到11966行)。
X86InstrCMovSetCC.td:对X86条件move及设置条件指令的描述。
X86InstrCompiler.td:由编译器使用的各种伪指令,及指令选择过程中使用的Pat模式。
X86InstrControl.td:描述X86 jump,return,call指令。
X86RegisterInfo.td:对X86体系寄存器的描述。
X86SchedHaswell.td:对Haswell机器模型的描述。
X86SchedSandyBridge.td:对Sandy Bridge机器模型的描述。
X86Schedule.td:X86体系指令调度的一般描述。
X86ScheduleAtom.td:用于Intel Atom处理器指令调度。
X86SchedSandyBridge.td:用于Sandy Bridge机器模型的指令调度。
X86SchedHaswell.td:用于Haswell机器模型的指令调度。
X86ScheduleSLM.td:用于Intel Silvermont机器模型的指令调度。
X86ScheduleBtVer2.td:用于AMD btver2 (Jaguar) 机器模型的指令调度。
7.0增加了以下文件:
X86SchedBroadwell.td::用于Broadwell机器模型的指令调度。
X86SchedSkylakeClient.td:用于Skylake Client机器模型的指令调度。
X86SchedSkylakeServer.td:用于Skylake Server机器模型的指令调度。
X86ScheduleZnver1.td:用于Znver1机器模型的指令调度。
X86SchedPredicates.td:调度谓词。用于识别依赖打破指令(dependency-breaking instruction)。
X86InstrVecCompiler.td:编译器使用的各种向量伪指令。
X86PfmCounters.td:各子架构可用的硬件计数器。
X86RegisterBanks.td:寄存器库的描述。
如此复杂的描述显然与X86是一个CISC机器,而且具有多种架构有关。由此也可以看出Tablegen实在是一个强大的工具。
另外,在目录llvm/include/llvm/target下包含了与体系架构无关的、公用的描述定义:
Target.td:每个目标机器都要实现的体系架构无关的接口。
TargetItinerary.td:使用instruction itineraries进行调度的机器所实现的体系架构无关的调度接口。
TargetSchedule.td:使用基于Tablegen调度的机器所实现的体系架构无关的调度接口。
TargetSelectionDAG.td:SelectionDAG指令选择调度器使用的体系架构无关的调度接口。
TargetCallingConv.td:用于描述调用惯例的体系架构无关的接口。
7.0增加以下文件:
GenericOpcodes.td:GlobalISel使用的通用操作码(7.0使用新的指令选择架构——GlobalISel)
TargetInstrPredicate.td:用于指令操作码/操作数上的限制。
在目录llvm/include/llvm/CodeGen下包含文件ValueTypes.td用于描述寄存器与操作数的类型。这是在整个LLVM范围内公用的(7.0因为使用GlobalIsel,还有目录llvm/include/llvm/GlobalISel,包含文件RegisterBank.td,Target.td与SelectionDAGCompat.td。SelectionDAGCompat.td描述了SDNode的GlobalISel等价定义。CodeGen目录下除了ValueTypes.td,还包含SDNodeProperties.td用于描述SDNode属性)。
在目录llvm/include/llvm/IR下包含文件Intrinsics.td用于描述通用的固有函数,以及与体系架构相关的固有函数,以X86为例,这个文件是IntrinsicsX86.td。另外,文件IntrinsicsNVVM.td描述了NVIDIA NVPTX的固有函数。文件IntrinsicsAArch64.td则是描述了64位系统的固有函数(7.0增加了新目标框架的对应文件)。
编译的本质是将高级语言写成的程序翻译为一个目标机器的指令序列,保证不改变语义,同时尽可能使该指令序列能在最小的执行时间内执行完成。为了尽快地执行指令,CPU使出了浑身解数,比如流水线,比如VLIW,比如向量机,等等。因此在编译器眼里,指令不只是一串二进制码那么简单。为了充分利用CPU提供的性能加速,编译器必须了解每一条指令执行所需的时间、所需的CPU资源(寄存器、计算单元、流水线占用等)、寄存器与内存使用的约束条件等等。另一方面,编译器也需要知道目标机器的种种细节,包括有哪些寄存器,这些寄存器的用途,有哪些功能单元,功能单元的性能、时序,诸如此类。
编译器利用这些信息进行:
1.1. DAG指令选择生成器
在LLVM的在线文档“The LLVM Target-Independent Code Generator”这样描述指令选择生成器:
TableGen DAG指令选择器生成器读入.td文件中的指令模式,并自动为你的目标机器构建模式匹配代码的各个部分。它有以下优点:
// Arbitrary immediate support. Implement in terms of LIS/ORI.
def : Pat<(i32 imm:$imm),
(ORI (LIS (HI16 imm:$imm)), (LO16 imm:$imm))>;
如果向寄存器载入一个立即数的单条指令模式都匹配失败,将使用这个模式。这个规则规定“配一个任意的i32立即数,把它转换为一个ORI指令(或一个16位立即数)及一个LIS指令(载入16位立即数,并且偏移左16位)”。要使这可行,使用LO16/HI16节点转换来操作输入的立即数(这时,获取该立即数的高16位或低16位)。
def STWU : DForm_1<37, (outs ptr_rc:$ea_res), (ins GPRC:$rS, memri:$dst),
"stwu $rS, $dst", LdStStoreUpd, []>,
RegConstraint<"$dst.reg = $ea_res">, NoEncode<"$ea_res">;
def : Pat<(pre_store GPRC:$rS, ptr_rc:$ptrreg, iaddroff:$ptroff),
(STWU GPRC:$rS, iaddroff:$ptroff, ptr_rc:$ptrreg)>;
这里,ptroff与ptrreg这对操作数被匹配到STWU指令中类型为memri的复杂操作数dst身上。(作者注:STWU的“(ins GPRC:$rS, memri:$dst)”赋值给基类Instruction的InOperandList,而“(STWU GPRC:$rS, iaddroff:$ptroff, ptr_rc:$ptrreg)”则是所谓的结果模式,即匹配得到的指令。STWU后是输入参数,即定义中的ins部分。其中memri是Operand的派生定义,在其定义里MIOperandInfo = (ops dispRI:$imm, ptr_rc_nor0:$reg),即memri:$dst是包含两个参数dispRI:$imm与ptr_rc_nor0:$reg的dag。参数的匹配关系是按位置一一对应,即dispRI:$imm匹配iaddroff:$ptroff,ptr_rc_nor0:$reg匹配ptr_rc:$ptrreg)。
尽管拥有许多长处,该系统当前也有一些局限,主要因为它仍在进行,尚未完成:
尽管有这些局限,指令选择器生成器对于典型指令集中大多数二元操作与逻辑操作仍然十分有用。如果你遇到任何问题,或不知道怎么做,请告诉Chris(作者注:Chris Lattner,LLVM项目的创始人)!
1.2. SelectionDAG
文档“The LLVM Target-Independent Code Generator”是这样介绍SelectionDAG的:
SelectionDAG对代码表示提供了一个抽象,以经得起使用自动化技术的指令选择(比如,基于模式匹配优化选择器的动态规划)检验的方式 。它还良好地适应了代码生成的其他阶段;特别的,指令调度(SelectionDAG非常接近于选择后DAG调度)。另外,SelectionDAG提供了一个宿主表示,在其中可以执行各式各样、非常低级(但和目标机器无关)的优化;这要求关于目标机器高效支持指令的大量信息。
SelectionDAG是一个有向无环图,其节点是SDNode的派生实例。SDNode的主要载荷是表示该节点执行哪个操作的操作码,以及操作数。在文件include/llvm/CodeGen/SelectionDAGNodes.h的开头描述了各种操作节点类型。
虽然大多数操作定义了单个值,在图中的每个节点可能定义了多个值。例如,一个复合的div/rem操作同时定义了商与余数。许多其他情形同样要求多个值。每个节点还有若干操作数,它们是通向定义了这个使用值节点的边。因为节点可能定义多个值,边由类SDValue的实例表示,它是一对
SelectionDAG包含两种类型的值:表示数据流的,以及表示控制流依赖性的。数据值是具有一个整数或浮点值类型的简单边。控制边被表示作“链(chain)”边,具有类型MVT::Other。这些边在具有副作用的节点间(比如load,store,call,return等)提供了一个次序。所有具有副作用的节点接受一个符号链作为输入,并产生一个新的符号链作为输出。按照惯例,符号链输入总是操作数0,而一个操作产生的最后的值总是链结果。不过,在指令选择后,在机器节点(指MemSDNode)中链在指令操作数后,后面可能跟着黏结节点。
一个SelectionDAG具有指定的“入口,Entry”及“根,Root”节点。入口节点总是一个具有操作码ISD::EntryToken的标记节点。根节点是符号链中最终副作用节点。例如,在一个单个基本块函数中,它就是返回节点。
SelectionDAG的一个重要的概念是“合法”与“非法”观念。目标机器合法的DAG仅使用支持的操作以及支持的类型。例如在一台32位PowerPC上,具有类型i1,i8,i16或i64类型值的DAG是非法的,使用SREM或UREM操作的DAG也是。类型与操作合法化阶段负责把非法的DAG转换为合法的DAG。
基于SelectionDAG的指令选择包含以下步骤:
在所有这些步骤完成后,SelectionDAG被摧毁,运行余下的代码生成遍。
在这一篇文档里是这样说SelectionDAG的来由:
初始的SelectionDAG是SelectionDAGBuilder类从LLVM输入由轻信的窥孔展开得到。这个遍的目的是向SelectionDAG尽可能多地展露低级、目标机器特定的细节。这个遍几乎是完全写死的(比如一个LLVM add转换为一个SDNode add,而一个getelementptr展开为平淡无奇的算术)。这个遍要求目标机器特定的钩子来降级“调用”、“返回”、“变长参数列表”等。对这些特性,使用TargetLowering接口。
1.3. v7.0的变化
文档《Global Instruction Selection》是这样介绍GlobalISel的:
GlobalISel是一个框架,它为指令选择提供一组可重用的遍与实用程序——从LLVM IR转换到目标特定的机器IR(MIR)。
GlobalISel目的在于替代SelectionDAG与FastISel,以解决3个主要问题:
GlobalISel直接工作在代码生成器余下部分使用的post-isel表示,MIR。它要求一个表示支持任意输入IR的扩展:通用机器IR(Generic Machine IR)。
GlobalISel工作在整个函数上。
GlobalISel是以允许代码重用的方式构建的。例如,优化与快速选择器都共享核心流水线(Core Pipeline),目标机器可以配置该流水线来更好配合它们的需要。