llvm学习笔记(更新1)

1. 概述

指令选择(instruction selection,也称为代码选择,有时甚至称为代码生成)是编译器代码生成器后端所涉及的重要问题之一。另外两个重要问题是指令调度与寄存器分配。指令选择器负责通过尽可能地用好可用的机器指令,将程序从目标机器无关的表示翻译到一个目标机器特定的形式。这使得两个正交的子问题必须得到解决:

  1. 检测何时及何地使用某条机器指令是可能的,并且
  2. 在存在多个选项时,决定选择哪条指令。

实际上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.tdGlobalISel使用的通用操作码(7.0使用新的指令选择架构——GlobalISel

TargetInstrPredicate.td:用于指令操作码/操作数上的限制。

在目录llvm/include/llvm/CodeGen下包含文件ValueTypes.td用于描述寄存器与操作数的类型。这是在整个LLVM范围内公用的7.0因为使用GlobalIsel,还有目录llvm/include/llvm/GlobalISel,包含文件RegisterBank.tdTarget.tdSelectionDAGCompat.tdSelectionDAGCompat.td描述了SDNodeGlobalISel等价定义。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资源(寄存器、计算单元、流水线占用等)、寄存器与内存使用的约束条件等等。另一方面,编译器也需要知道目标机器的种种细节,包括有哪些寄存器,这些寄存器的用途,有哪些功能单元,功能单元的性能、时序,诸如此类。

编译器利用这些信息进行:

  • 指令选择——选择既能完成指定操作,执行时间又最短的指令。
  • 指令调度——根据指令间的依赖关系(不像在高级语言程序中出现的几种语句间依赖关系,这里依赖关系有:输出依赖——一条指令的输出作为另一条指令的输入;功能单元的竞争:功能单元的执行有一定的时间周期,而且只能同时容纳一条指令),进行指令重排,以期更高效地利用CPU的功能单元。
  • 寄存器分配——在指令选择过程中,寄存器被认为是无限的,但实际机器总是只有有限的寄存器,而且这些寄存器也一定各有用途。因此,编译器必须保证目标机器的寄存器集能满足产生的指令序列。显然,指令选择期间必须将使用了多少寄存器、使用哪些寄存器作为选择指令的一个指标。

1.1. DAG指令选择生成器

在LLVM的在线文档“The LLVM Target-Independent Code Generator”这样描述指令选择生成器:

TableGen DAG指令选择器生成器读入.td文件中的指令模式,并自动为你的目标机器构建模式匹配代码的各个部分。它有以下优点:

  • 在编译编译器时刻,它分析你的指令模式并告诉你你的模式是否合理。
  • 它可以处理用于模式匹配的操作数上任意的约束。特别的,描述类似“匹配任意13位符号扩展值的立即数”,它是直观的。例如,参考PowerPC后端的immSExt16及相关的tblgen类。
  • 它知道模式定义的几个重要的特性。例如,它知道加法是符合交换律的,因此它允许上面的FMADDS模式匹配“(fadd X, (fmul Y, Z))”以及“(fadd (fmul X, Y), Z)”,不需要目标机器作者特殊地处理这个案例。
  • 它具有完善的类型推导系统。特别的,你很少需要明确告诉系统你模式各部分是什么类型。在上面的FMADDS例子中,我们不需要告诉tblgen模式中所有的节点具有类型‘f32’。从F4RC(作者注:这是一个寄存器类别)具有类型‘f32’这个事实,它能推导并传播这个认识。
  • 目标机器可以定义自己的(并且依赖内置的)“模式片段”。模式片段是可重用的模式块,在编译编译器时刻内联进你的模式。例如,整形的“(not x)”操作实际上被定义为一个展开为“(xor x, -1)”的模式片段,因为SelectionDAG没有一个原生的‘not’操作。目标机器可以视情况定义自己的速记片段。例子参考‘not’及‘ineg’的定义。
  • 除了指令,目标机器可以使用‘Pat’类详细说明任意映射到一条或多条指令的模式。例如,PowerPC没有办法在一条指令中载入一个任意整形立即数。要告诉tblgen如何做到,这样定义:(作者注:在这里Pattern与Pat没有本质区别,Pat只是在第二个模板参数外比Pattern少写一对“[]”)
// 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位)。

  • 在使用‘Pat’类把一个模式映射到具有一个或多个复杂操作数的一条指令(比如像X86的寻址模式)时,该模式可以使用一个ComplexPattern来整体说明操作数,或者它也可以分开地说明复杂操作数的组成。后者的实现有PowerPC后端的前置递增指令:
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部分。其中memriOperand的派生定义,在其定义里MIOperandInfo = (ops dispRI:$imm, ptr_rc_nor0:$reg),即memri:$dst是包含两个参数dispRI:$immptr_rc_nor0:$regdag。参数的匹配关系是按位置一一对应,即dispRI:$imm匹配iaddroff:$ptroffptr_rc_nor0:$reg匹配ptr_rc:$ptrreg)。

  • 尽管系统自动做了许多工作,如果存在难以表达的对象,它仍然允许你编写定制的C++代码来匹配特殊的情形。

尽管拥有许多长处,该系统当前也有一些局限,主要因为它仍在进行,尚未完成:

  • 总体上,没有办法定义或匹配定义了多个值的SelectionDAG节点(比如SMUL_LOHI,LOAD,CALL等)。这是目前你仍然需要为你的指令选择器编写定制C++代码的原因。
  • 还没有好的方式来支持匹配复杂的取址模式。在未来,我们将扩展模式片段以允许它们定义多个值(比如X86取址模式中的四个操作数,目前它们由定制C++代码匹配)。另外,我们将扩展片段,使一个片段可以匹配多个不同的模式。
  • 我们还不能自动地推导像isStore/isLoad这样的标记。
  • 还不能为Legalizer自动地产生支持的寄存器及操作的集合。
  • 还没有办法把定制的与合法化的节点联系起来。

尽管有这些局限,指令选择器生成器对于典型指令集中大多数二元操作与逻辑操作仍然十分有用。如果你遇到任何问题,或不知道怎么做,请告诉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的实例表示,它是一对<SDNode, unsigned>,分别表示节点及要被使用的结果值。由一个SDNode产生的每个值具有一个关联的MVT(机器值类型,Machine Value Type),以表示该值的类型。

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的指令选择包含以下步骤:

  1. 构建初始DAG——这个步骤执行从输入的LLVM代码到一个非法SelectionDAG的简单转换。
  2. 优化SelectionDAG——这个步骤在SelectionDAG上执行简单的优化以简化之,并为支持元操作的目标机器识别元指令(像rotate及div/rem对)。这使得结果代码更加高效,并使得从DAG选择指令更简单。
  3. 合法化SelectionDAG类型——这个步骤消除任何目标机器不支持的类型。
  4. 优化SelectionDAG——运行SelectionDAG优化器以清除类型合法化造成的重复。
  5. 合法化SelectionDAG操作——这个步骤消除任何目标机器不支持的操作。
  6. 优化SelectionDAG——运行SelectionDAG优化器以清除操作合法化造成的重复。
  7. 从DAG选择指令——最后,目标机器指令选择器匹配DAG操作到目标机器指令。这个过程把目标机器无关的DAG输入翻译到另一个目标机器指令的DAG。
  8. SelectionDAG调度与编队——最后这个阶段向目标机器指令DAG中的指令赋予一个线性次序,并把它们送入正在编译的MachineFunction。这个步骤使用传统的prepass scheduling技术。

在所有这些步骤完成后,SelectionDAG被摧毁,运行余下的代码生成遍。

在这一篇文档里是这样说SelectionDAG的来由:

初始的SelectionDAG是SelectionDAGBuilder类从LLVM输入由轻信的窥孔展开得到。这个遍的目的是向SelectionDAG尽可能多地展露低级、目标机器特定的细节。这个遍几乎是完全写死的(比如一个LLVM add转换为一个SDNode add,而一个getelementptr展开为平淡无奇的算术)。这个遍要求目标机器特定的钩子来降级“调用”、“返回”、“变长参数列表”等。对这些特性,使用TargetLowering接口。

1.3. v7.0的变化

文档《Global Instruction Selection》是这样介绍GlobalISel的:

GlobalISel是一个框架,它为指令选择提供一组可重用的遍与实用程序——从LLVM IR转换到目标特定的机器IRMIR)。

GlobalISel目的在于替代SelectionDAGFastISel,以解决3个主要问题:

  • 性能——SelectionDAG引入一个专有的中间表示,这有编译时的代价。

GlobalISel直接工作在代码生成器余下部分使用的post-isel表示,MIR。它要求一个表示支持任意输入IR的扩展:通用机器IRGeneric Machine IR)。

  • 粒度——SelectionDAGFastISel工作在单独的基本块上,丢失了一些全局优化机会。

GlobalISel工作在整个函数上。

  • 模块化——SelectionDAGFastISel差别显著,共享很少代码。

GlobalISel是以允许代码重用的方式构建的。例如,优化与快速选择器都共享核心流水线(Core Pipeline),目标机器可以配置该流水线来更好配合它们的需要。

  • 9
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值