LLVM学习笔记(52)

Posted wuhui_gdnt

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LLVM学习笔记(52)相关的知识,希望对你有一定的参考价值。

3.11. GlobalISel代码的自动生成(v7.0)

3.11.1. 概述

GlobalISelLLVM的一个新锐指令选择器。关于它的描述,首先是LLVM在线文档

第一部分参考v7.0的变化一节。

通用机器IR

机器IR工作在物理寄存器、寄存器类,以及(大部分情形)目标机器特定指令上。

为了填补LLVM IR的空缺,GlobalISel引入了机器IR的“通用”扩展:

  • 通用指令
  • 通用虚拟寄存器
  • 寄存器组(Register Bank
  • 低级类型(Lower Level TypeLLT

注意:通用MIRGMIR)表示仍然包含了对IR构造的引用(比如GlobalValue)。消除这些将使我们写出更准确的测试,或者在构建最初的MIR后删除IR。不过,这不是GlobalISel着力之处。

通用指令

主要的扩展是对pre-isel的通用机器指令的支持(比如G_ADD)。像其他目标机器无关指令(比如COPYPHI),在所有目标机器上可用。

TODO:虽然我们持续加入指令,一种指令特别地暴露了有趣的问题:比较以及如何表示条件码。某些目标机器(x86ARM)有设置多个标记的通用比较,由各种预测使用。其他(IR)在比较中指明谓词,使用者仅获得一个比特。SelectionDAG分别使用SETCC/CONDBRBR_CC(对选择类似)来表示这。

MachineIRBuilder封装了MachineInstrBuilder,提供了创建这些通用指令的便利方法。

通用虚拟寄存器

通用指令工作在一种新寄存器上:“通用”虚拟寄存器。与非通用虚拟寄存器相反,它们没有被分配一个寄存器类。相反,通用虚拟寄存器有一个低级类型,可以分配一个寄存器组。

像处理非通用虚拟寄存器那样,MachineRegisterInfo追踪相同的信息(比如use-def链)。另外,它还追踪寄存器的低级类型,以及RegisterBank,而不是TargetRegisterClass,如果有的话。

为了简单起见,大多数通用指令仅接受通用虚拟寄存器:

  • 不使用立即数,它们使用一个由一条指令具现的该立即数定义的通用虚拟寄存器(参考常量降级)。
  • 不使用物理寄存器,它们使用由一个COPY定义的通用虚拟寄存器。

注意:我们以一个替代的表示开始,其中MRI追踪每个通用虚拟寄存器的大小,指令有若干组类型。这有两个缺陷:类型与大小是重复的,没有通用的方式来获取一个指定操作数的类型(因为在指令类型与操作数间没有一一映射)。我们考虑在MCInstrDesc的某个变形中加入类型:参考PR26576[GlobalISel] Generic MachineInstrs need a type but this increases the memory footprint of the related objects

寄存器组

参考上面RegisterBank代码的自动生成(v7.0)里的描述。

低级类型

每个通用虚拟寄存器具有类型,由一个LLT类的实例表示。

类似于EVT/MVT/Type,它不区分无符号与有符号整数类型。另外,它也不区分整数与浮点类型:它主要携带绝对必需的信息,比如大小以及向量通道数:

  • sN用于标量
  • pN用于指针
  • <N x sM>用于向量
  • unsized用于标签等

LLT目的在于在SelectionDAG中替换EVT

下面是一些LLT例子,以及它们等价的EVTType

LLT

EVT

IR Type

s1

i1

i1

s8

i8

i8

s32

i32

i32

s32

f32

float

s17

i17

i17

s16

N/A

i8, i8

s32

N/A

[4 x i8]

p0

iPTR

i8*, i32*, %opaque*

p2

iPTR

i8 addrspace(2)*

<4 x s32>

v4f32

<4 x float>

s64

v1f64

<1 x double>

<3 x s32>

v3i23

<3 x i32>

unsized

Other

label

基本原理:指令已经编码了类型的一个特定解释(比如addfadd,或者sdivudiv)。另外,在类型系统中编码这个信息需要对选择器而言没有益处的bitcast

指针类型由地址空间来区分。这与IR相配,与SelectionDAG中地址空间是操作的一个属性相左。这个表示更好地支持依赖于地址空间的不同大小的指针。

注意:当前,LLT要求向量里至少2个元素,但一些目标机器有“单元素向量”的概念。把它们表示为底下的标量类型是一个良好的简化。

TODO:当前,非通用虚拟寄存器,由非pre-isel通用指令定义,不能有类型,因此不能被一条pre-isel指令使用。取而代之,使用一个COPY向它们给出一个类型。我们可以放松这个要求,允许所有虚拟寄存器有类型:在流水线早期发布目标机器特定MIR时,这将减少所要求的MI数量。这应该纯粹是一个编译时优化。

核心流水线

4个所要求的遍,与优化模式无关:

  • IRTranslator
    • APICallLowering
    • 聚集类型
    • 常量降级
  • Legalizer
    • APILegalizerInfo
    • 2次幂类型
    • 向量类型
  • RegBankSelect
    • APIRegisterBankInfo
    • RegBankSelect模式
  • InstructionSelect
    • APIInstructionSelector
    • SelectionDAG规则导入
    • PatLeaf谓词
    • 定制SDNode
    • ComplexPattern

可以在更高的优化层次或者对特定目标机器插入额外的遍。例如,匹配当前SelectionDAG转换集合:MachineCSE与每个遍之间一个更好的MachineCombiner

注意:理论上,不是所有的遍都是必需的。作为一个额外的编译时优化,通过设置相关的MachineFunction属性,我们可以跳过某些遍。例如,如果IRTranslator没有遇到任何非法指令,它将设置legalized属性来避免运行Legalizer。类似的,我们考虑在每目标机器基础上特化IRTranslator来直接发布目标机器特定的MI。不过,我们决定保持核心流水线简单,集中精力在no-op情形里尽可能减小遍的开销。

IRTranslator

这个遍将输入的LLVM IR Function翻译为一个GMIR MachineFunction

TODO:当前这不支持更复杂的指令,特别是涉及控制流的那些(switchinvoke,……)。特别对于switch,我们可以一开始使用LowerSwitch遍。

APICallLowering

IRTranslator(使用目标机器提供的CallLowering)通过将调用、返回及实参降级(lowering)到合适的寄存器使用以及指令序列,也实现了该ABI的调用惯例。

聚集类型

聚集类型被降级为单个标量虚拟寄存器。这不同于通过GetValueVTs得到的SelectionDAG的多虚拟寄存器。

TODO:因为某些比特位是未定义的(填充位),我们应该考虑以额外的元数据扩展该表示(实际上,虚拟寄存器上缓存了computeKnownBits信息)。参考PR26161[GlobalISel] Value to vreg during IR to MachineInstr translation for aggregate type

常量降级

IRTranslator将常量操作数降级到由G_CONSTANTG_FCONSTANT指令定义的通用虚拟寄存器的使用。当前这些指令总是发布在入口基本块。在一个MachineFunction中,每个常量由单个通用虚拟寄存器具现。

这是有利的,因为它允许我们在指令选择期间将常量折叠为立即数操作数,同时仍然避免了对高代价非可折叠常量重复具现。不过,在一个-O流水线中,这会导致不必要的溅出与重载,因为这些虚拟寄存器可以有长的生命期。

TODO:我们正在调查,以快速以及优化的模式,放置这些指令更好的地方。

Legalizer

这个遍转换通用机器指令,使之合法。

合法指令被定义为:

  • 可选择——目标机器后面能够把它选择为目标机器特定(非通用)指令。
  • 工作在可以载入及保存的虚拟寄存器上——如果必要,对每个通用虚拟寄存器操作数,目标机器可以选择一个G_LOAD/G_STORE

SelectionDAG相反,不存在合法化遍。特别的,‘类型’与‘操作’合法化不是分开的。

合法化是迭代的,所有的状态包含在GMIR中。为了维护中间代码的有效性,引入指令:

  • G_MERGE_VALUES——将多个相同大小的寄存器合并为单个更大的寄存器。
  • G_UNMERGE_VALUES——从单个更大的寄存器获取多个相同大小的寄存器。
  • G_EXTRACT——从单个更大的寄存器获取一个简单寄存器(作为连续比特序列)。

因为预期为合法化过程的副产品,它们在Legalizer遍的末尾合并。如果有任何遗留,它们预期总是可选择的,必要时使用载入及保存。

APILegalizerInfo

当前,这个API大体上类似于SelectionDAG/TargetLowering,但以两个方式扩展:

  • 可用活动的集合更宽泛,避免了当前过分重载的Expand(这可以涵盖从libcall到依赖于节点操作码的标量化)。
  • 因为没有独立的类型合法化,一条指令上单独变化的类型可以有独立的活动。例如,G_ICMP2个独立的类型:结果与输入;我们需要能够表达比较232sOK的,但必须以另一个方式处理s1的结果。

这样,在决定做什么时的关键是InstrAspect,本质上一个包含(Opcode, TypeIdx, Type)并映射到一个建议的行动方针的元组。

一个样例使用可以是:

// The CPU can't deal with an s1 result, do something about it.
setAction(G_ICMP, 0, s1, WidenScalar);
// An s32 input (the second type) is fine though.
setAction(G_ICMP, 1, s32, Legal);

TODO:一个值得调查的替代方案是推广这个API来表示活动,使用实现这个活动的std:: function,而不是显式的enum符号(LegalWidenScalar,……)。【作者注:V7.0里已经实现了这一点,并在进一步完善中,参考后面有关LegalizeRule的章节】

TODO:另外,我们可以使用TableGen从现有的模式一开始推导操作的合法性(因为任何我们可以选择的模式根据定义是合法的)。扩展这来描述合法化活动,是一个大得多,但潜在有用的项目。

2次幂类型

TODO:当前不支持大小不是2次幂的类型。可能要求改变setAction API来支持它们。即使名义上明确地说明操作也仅给出像“加宽”或“收窄”的建议。最终类型仍然没有指明,通过重复加倍/减半类型大小来执行一个检索。对不是2次幂的类型这是不对的。有理由期望我们可以构造一组高效的旁表(side-tables),编码一个从整数(如当前类型大小)到类型(合法大小)的映射,用于更通用的查找。

向量类型

向量首先合法化它们的元素类型:<A x sB>变成<A x sC>使至少一个使用sC的操作成为合法。

当前,这通过函数setScalarInVectorAction来说明,例如调用为:

setScalarVectorAcction(G_ICMP, s1, WidenScalar);

接着选择元素数,使得整个操作合法。这时这方面是不可控的,但可能应该可以(你可以想象在一个<2 x s8>操作是否应该被向量化或扩展为<8 x s8>上的分歧)。

RegBankSelect

这个遍把通用指令的通用虚拟寄存器限制在某个寄存器组。

它迭代地将指令映射到一组每操作数的组分配。可能的映射由目标机器提供的RegisterBankInfo确定。然后应用这个映射,如果必要可以引入COPY指令。

它自顶向下遍历MachineFunction,使得在分析一条指令时,所有的操作数都已经被映射。

在有利时,这个遍也可以重新映射目标机器特定的指令。在将来,这可以替代ExeDepsFix遍,因为对在多个组上可用的一条指令,我们可以直接选择最好的变种。

APIRegisterBankInfo

RegisterBankInfo描述了寄存器组的多个方面。

  • 组:addRegBankCoverage——哪个寄存器组覆盖每个寄存器类。
  • 跨组拷贝:copyCost——从一个组到另一个COPY的代价。
  • 缺省映射:getInstrMapping——对一条给定指令,缺省的组分配。
  • 替代映射:getInstrAlternativeMapping——对一条给定指令,其他可能的组分配。

TODO:所有这些信息最终应该是静态的,且由TableGen生成,主要使用由组描述扩展的现有信息。

TODOgetInstrMapping当前与getInstrAlternativeMapping分开,因为后者代价更高:随着我们迁移到静态映射信息,这两个方法都将是没有代价的,且我们应该合并它们。

RegBankSelect模式

当前RegBankSelect有两个模式:

  • 快速——对每条指令,挑选一个目标机器提供的“缺省”组分配。这是-O0的缺省。
  • 贪婪——对每条指令,挑选几个目标机器提供的组分配候选方案中代价最低的。

我们目的在于最终引入一个额外的优化模式:

  • 全局——跨过多条指令,挑选代价最低的组分配组合。

注意:在AArch64上,我们正在考虑在-O0使用贪婪模式(或者可能在后端-O1):因为低级类型不区分浮点与整形标量,对载入及保存的缺省分配是整形组,在大多数浮点操作上引入跨组拷贝。

InstructionSelect

这个遍将通用机器指令转换为等价的目标机器特定的指令。它自顶向下遍历MachineFunction,选择定义前(before definition)的使用,启用平凡的死代码消除。

APIInstructionSelector

目标机器实现类InstructionSelector,包含目标机器特定的选择逻辑。

实例由子目标机器提供,使得可以根据子目标机器特性特化选择器(比如,一个重载一个通用公共选择器部分的向量选择器)。我们还可能希望通过MachineFunction参数化它,启用基于像optsize的函数属性的选择器变形。

样例API包含:

virtual bool select(MachineInstr &MI)

这个目标机器提供的方法负责将一个可能通用的MI突变(mutating)(或替换)为一个完全目标机器特定的等价对象。它还负责执行把通用虚拟寄存器的必要限制翻译成合适的寄存器类,以及向寄存器分配器传递COPY指令。

通过遍历虚拟寄存器操作数的use-def链,InstructionSelector可以将其他指令折叠为被选中的MI。因为GlobalISel是全局的,这个折叠可以跨基本块出现。

SelectionDAG规则导入

TableGen将导入SelectionDAG规则,并提供以下函数来执行它们:

bool selectImpl(MachineInstr &MI)

选项-stats可用于确定规则的多大比例被成功导入。使用这最简单的方式是从ninja -v拷贝-gen-globalisel TableGen命令并修改它。

类似地,选项-warn-on-skipped-patterns可用于获取规则没被导入的原因。这可用于关注最重要的拒绝理由。

PatLeaf谓词

PatLeaf不能导入,因为它们的C++实现是SDNode对象的形式。处理立即数谓词的PatLeaf应该视情况被ImmLeafIntImmLeafFPImmLeaf替换。

对其他PatLeaf没有标准答案。某些标准的谓词已经被构造进TableGen,但一般不应该这样做。

定制SDNode

应该使用GINodeEquiv将定制SDNode映射到目标机器伪指令(target pseduo)。这会导致指令选择器导入它们,但你还需要确保这些目标机器伪指令在这个指令选择器之前被加入MIR

任何之前的遍都是合适的,但合法化器将是特别常见的选择。

ComplexPattern

ComplexPattern不能导入,因为它们的C++实现是SDNode对象的形式。GlobalISel版本应该通过GIComplexOperandMatcher定义,并通过GIComplexPatternEquiv映射到ComplexPattern

对移植ComplexPattern,以下谓词是有用的:

  • isBaseWithConstantOffset()——检查基址+偏移结构体
  • isOperandImmEqual()——检查一个特定的常量
  • isObviouslySafeToFold()——检查一条指令不能下移并折叠入另一条指令的原因。

C++实现这些是重点:

  • 不要在谓词里修改MIR
  • 渲染器lambda应该由值捕捉,以避免使用后释放。它们将在谓词返回后被使用。
  • 仅在渲染器lambda中创建指令。GlobalISel将不能清除你创建但不使用的东西。

维护性

迭代性转换

遍被分解为小的、迭代性的转换,所有的状态以MIR表示。

这不同于使用各种内存中旁表的SelectionDAG(特别是合法化器)。

MIR序列化

通用机器IR是可序列化的(参考Machine IR (MIR) Format Reference Manual)。结合迭代式转换,这使得更细致的测试成为可能,而不是要求大的且脆弱的IR到汇编的测试。

在核心流水线中当前“阶段”由一组MachineFunctionProperties来表示:

  • legalized
  • regBankSelected
  • selected

MachineVerifier

遍做法让我们使用MachineVerifier来强制不变量。例如,regBankSelected方法可能没有不附属组的通用虚拟寄存器。

TODOMachineVerifier是整体的,我们希望执行的某些检查不能整合进去:GlobalISel是独立的库,因此我们不能直接从CodeGen引用它。例如,合法性检查当前在恰当的RegBankSelect/ InstructionSelect中完成。我们可以在检查外添加#ifdef,或添加某种验证器API

进展与将来的工作

最初的目标是在AArch64上替代FastISel。下一步将是作为优化的ISel替换SelectionDAG

注意:在遍历GlobalISel时,我们努力避免影响SelectionDAGFastISel或其他MIR遍的性能。例如,通用虚拟寄存器的类型保存在MachineRegisterInfo中的一张独立表里,在InstructionSelect后被销毁。

FastISel替换

对最初的FastISel替换,我们的目的是在选择失败时,回退到SelectionDAG

当前,快速流水线的编译时间在FastISel1.5倍之内。我们对达到1.1/1.2倍以内感到乐观,但鉴于多遍的做法,击败FastISel将是极有挑战性的。不过,支持所有的IR(通过一个完整的合法化器),避免在最坏情形里回退到SelectionDAG,将比SelectionDAG+FastISel,能更好地分摊性能。

注意:我们考虑从不回退到SelectionDAG,相反及早决定给定的函数GlobalISel是否支持。这个决定将基于合法化器的查询。我们因为两个原因放弃回退:1)在IR输入上,我们需要模拟IRTranslator2)对没有预见的失败变得健壮,并使得迭代改进成为可能。

事实上,SelectionDAG是在SDNode上执行选择,生成MachineSDNode对象,在这些对象上进行若干处理后,再转换为MachineInstr对象。GlobIalISel则工作在MachineInstr上(严格地说,这个系统也工作在SDNode上,不过先通过IRTranslatorSDNode翻译为MachineInstr),执行MachineInstrMachineInstr的选择。

后面我们可以看到,GlobalISelSelectionDAG共用指令、目标机器的TD描述和处理,它们都是从PatternToMatch对象开始构建关键的匹配表,它们匹配表的构造也是类似的。因为GlobalISelMachineFunctionMachineBasicBlockMachineInstr上工作,可以使用它们的方法对MachineInstr进行处理,这提高了代码的重用性,也加速了指令的处理。

3.11.1.1. 指令的定义

.td文件层面,GlobalISel使用的指令定义的基类是StandardPseudoInstructionTarget.td):

915     class StandardPseudoInstruction : Instruction

916       let mayLoad = 0;

917       let mayStore = 0;

918       let isCodeGenOnly = 1;

919       let isPseudo = 1;

920       let hasNoSchedulingInfo = 1;

921       let Namespace = "TargetOpcode";

922    

StandardPseudoInstruction可以派生出目标机器无关的指令定义,如PHI。这些指令也在文件TargetOpcodes.def中描述。另外,GenericInstruction直接从StandardPseudoInstruction派生:

19       class GenericInstruction : StandardPseudoInstruction

GenericInstruction派生出G_ANYEXT等这些GlobalISel的指令定义。如:

23       def G_ANYEXT : GenericInstruction

24         let OutOperandList = (outs type0:$dst);

25         let InOperandList = (ins type1:$src);

26         let hasSideEffects = 0;

27      

其中OutOperandList指定的输出类型是下面的type0(类似的还有type1~type5),这就是所谓的Generic类型。GlobalISel指令使用这些类型并没有指定具体的类型,后面合法化的配置过程里,会给出具体类型以及合法化方案。这里,type0~type5只是用于表示不同的类型,也就是说一个指令定义可以最多使用6个不同类型。

这样的好处是无需再为使用具体不同的类型的指令给出各自的定义,像这里定义的G_ANYEXT就不依赖任何具体类型。

813     def type0 : TypedOperand<"OPERAND_GENERIC_0">;

注意在下面TypedOperand的定义中,基类Operand的参数是untyped,即对GlobalISel而言,这些操作数被视为无类型(其实是所谓的低级类型,LLT)。

808     class TypedOperand<string Ty> : Operand<untyped>

809       let OperandType = Ty;

810       bit IsPointer = 0;

811    

不过813行的TypedOperand的参数字符串”OPERAND_GENERIC_0”会导出到X86GenInstrInfo.inc中,它是MCInstrDesc.h里枚举类型OperandType的其中一个字面量。MCOperandInfo用它们来识别通用指令(参考MCOperandInfo:: isGenericType())。

GlobalISel的其他通用指令定义也出现在TD文件GenericOpcodes.td中。注意,这些指令定义都是目标机器无关的。同样,GlobalIS

以上是关于LLVM学习笔记(52)的主要内容,如果未能解决你的问题,请参考以下文章

LLVM 上的简单标量支持

LLVM学习笔记(43-2)

LLVM学习笔记(54)

LLVM学习笔记(54)

LLVM学习笔记(44-2)

LLVM学习笔记(53)