LLVM学习笔记(52)
Posted wuhui_gdnt
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LLVM学习笔记(52)相关的知识,希望对你有一定的参考价值。
3.11. GlobalISel代码的自动生成(v7.0)
3.11.1. 概述
GlobalISel是LLVM的一个新锐指令选择器。关于它的描述,首先是LLVM的在线文档:
第一部分参考v7.0的变化一节。 通用机器IR 机器IR工作在物理寄存器、寄存器类,以及(大部分情形)目标机器特定指令上。 为了填补LLVM IR的空缺,GlobalISel引入了机器IR的“通用”扩展:
注意:通用MIR(GMIR)表示仍然包含了对IR构造的引用(比如GlobalValue)。消除这些将使我们写出更准确的测试,或者在构建最初的MIR后删除IR。不过,这不是GlobalISel着力之处。 通用指令 主要的扩展是对pre-isel的通用机器指令的支持(比如G_ADD)。像其他目标机器无关指令(比如COPY或PHI),在所有目标机器上可用。 TODO:虽然我们持续加入指令,一种指令特别地暴露了有趣的问题:比较以及如何表示条件码。某些目标机器(x86,ARM)有设置多个标记的通用比较,由各种预测使用。其他(IR)在比较中指明谓词,使用者仅获得一个比特。SelectionDAG分别使用SETCC/CONDBR与BR_CC(对选择类似)来表示这。 类MachineIRBuilder封装了MachineInstrBuilder,提供了创建这些通用指令的便利方法。 通用虚拟寄存器 通用指令工作在一种新寄存器上:“通用”虚拟寄存器。与非通用虚拟寄存器相反,它们没有被分配一个寄存器类。相反,通用虚拟寄存器有一个低级类型,可以分配一个寄存器组。 像处理非通用虚拟寄存器那样,MachineRegisterInfo追踪相同的信息(比如use-def链)。另外,它还追踪寄存器的低级类型,以及RegisterBank,而不是TargetRegisterClass,如果有的话。 为了简单起见,大多数通用指令仅接受通用虚拟寄存器:
注意:我们以一个替代的表示开始,其中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,它不区分无符号与有符号整数类型。另外,它也不区分整数与浮点类型:它主要携带绝对必需的信息,比如大小以及向量通道数:
LLT目的在于在SelectionDAG中替换EVT。 下面是一些LLT例子,以及它们等价的EVT与Type:
基本原理:指令已经编码了类型的一个特定解释(比如add与fadd,或者sdiv与udiv)。另外,在类型系统中编码这个信息需要对选择器而言没有益处的bitcast。 指针类型由地址空间来区分。这与IR相配,与SelectionDAG中地址空间是操作的一个属性相左。这个表示更好地支持依赖于地址空间的不同大小的指针。 注意:当前,LLT要求向量里至少2个元素,但一些目标机器有“单元素向量”的概念。把它们表示为底下的标量类型是一个良好的简化。 TODO:当前,非通用虚拟寄存器,由非pre-isel通用指令定义,不能有类型,因此不能被一条pre-isel指令使用。取而代之,使用一个COPY向它们给出一个类型。我们可以放松这个要求,允许所有虚拟寄存器有类型:在流水线早期发布目标机器特定MIR时,这将减少所要求的MI数量。这应该纯粹是一个编译时优化。 核心流水线 有4个所要求的遍,与优化模式无关:
可以在更高的优化层次或者对特定目标机器插入额外的遍。例如,匹配当前SelectionDAG转换集合:MachineCSE与每个遍之间一个更好的MachineCombiner。 注意:理论上,不是所有的遍都是必需的。作为一个额外的编译时优化,通过设置相关的MachineFunction属性,我们可以跳过某些遍。例如,如果IRTranslator没有遇到任何非法指令,它将设置legalized属性来避免运行Legalizer。类似的,我们考虑在每目标机器基础上特化IRTranslator来直接发布目标机器特定的MI。不过,我们决定保持核心流水线简单,集中精力在no-op情形里尽可能减小遍的开销。 IRTranslator 这个遍将输入的LLVM IR Function翻译为一个GMIR MachineFunction。 TODO:当前这不支持更复杂的指令,特别是涉及控制流的那些(switch,invoke,……)。特别对于switch,我们可以一开始使用LowerSwitch遍。 API:CallLowering IRTranslator(使用目标机器提供的CallLowering)通过将调用、返回及实参降级(lowering)到合适的寄存器使用以及指令序列,也实现了该ABI的调用惯例。 聚集类型 聚集类型被降级为单个标量虚拟寄存器。这不同于通过GetValueVTs得到的SelectionDAG的多虚拟寄存器。 TODO:因为某些比特位是未定义的(填充位),我们应该考虑以额外的元数据扩展该表示(实际上,虚拟寄存器上缓存了computeKnownBits信息)。参考PR26161:[GlobalISel] Value to vreg during IR to MachineInstr translation for aggregate type 常量降级 IRTranslator将常量操作数降级到由G_CONSTANT或G_FCONSTANT指令定义的通用虚拟寄存器的使用。当前这些指令总是发布在入口基本块。在一个MachineFunction中,每个常量由单个通用虚拟寄存器具现。 这是有利的,因为它允许我们在指令选择期间将常量折叠为立即数操作数,同时仍然避免了对高代价非可折叠常量重复具现。不过,在一个-O流水线中,这会导致不必要的溅出与重载,因为这些虚拟寄存器可以有长的生命期。 TODO:我们正在调查,以快速以及优化的模式,放置这些指令更好的地方。 Legalizer 这个遍转换通用机器指令,使之合法。 合法指令被定义为:
与SelectionDAG相反,不存在合法化遍。特别的,‘类型’与‘操作’合法化不是分开的。 合法化是迭代的,所有的状态包含在GMIR中。为了维护中间代码的有效性,引入指令:
因为预期为合法化过程的副产品,它们在Legalizer遍的末尾合并。如果有任何遗留,它们预期总是可选择的,必要时使用载入及保存。 API:LegalizerInfo 当前,这个API大体上类似于SelectionDAG/TargetLowering,但以两个方式扩展:
这样,在决定做什么时的关键是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符号(Legal,WidenScalar,……)。【作者注: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遍,因为对在多个组上可用的一条指令,我们可以直接选择最好的变种。 API:RegisterBankInfo 类RegisterBankInfo描述了寄存器组的多个方面。
TODO:所有这些信息最终应该是静态的,且由TableGen生成,主要使用由组描述扩展的现有信息。 TODO:getInstrMapping当前与getInstrAlternativeMapping分开,因为后者代价更高:随着我们迁移到静态映射信息,这两个方法都将是没有代价的,且我们应该合并它们。 RegBankSelect模式 当前RegBankSelect有两个模式:
我们目的在于最终引入一个额外的优化模式:
注意:在AArch64上,我们正在考虑在-O0使用贪婪模式(或者可能在后端-O1):因为低级类型不区分浮点与整形标量,对载入及保存的缺省分配是整形组,在大多数浮点操作上引入跨组拷贝。 InstructionSelect 这个遍将通用机器指令转换为等价的目标机器特定的指令。它自顶向下遍历MachineFunction,选择定义前(before definition)的使用,启用平凡的死代码消除。 API:InstructionSelector 目标机器实现类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应该视情况被ImmLeaf,IntImmLeaf或FPImmLeaf替换。 对其他PatLeaf没有标准答案。某些标准的谓词已经被构造进TableGen,但一般不应该这样做。 定制SDNode 应该使用GINodeEquiv将定制SDNode映射到目标机器伪指令(target pseduo)。这会导致指令选择器导入它们,但你还需要确保这些目标机器伪指令在这个指令选择器之前被加入MIR。 任何之前的遍都是合适的,但合法化器将是特别常见的选择。 ComplexPattern ComplexPattern不能导入,因为它们的C++实现是SDNode对象的形式。GlobalISel版本应该通过GIComplexOperandMatcher定义,并通过GIComplexPatternEquiv映射到ComplexPattern。 对移植ComplexPattern,以下谓词是有用的:
对C++实现这些是重点:
维护性 迭代性转换 遍被分解为小的、迭代性的转换,所有的状态以MIR表示。 这不同于使用各种内存中旁表的SelectionDAG(特别是合法化器)。 MIR序列化 通用机器IR是可序列化的(参考Machine IR (MIR) Format Reference Manual)。结合迭代式转换,这使得更细致的测试成为可能,而不是要求大的且脆弱的IR到汇编的测试。 在核心流水线中当前“阶段”由一组MachineFunctionProperties来表示:
MachineVerifier 遍做法让我们使用MachineVerifier来强制不变量。例如,regBankSelected方法可能没有不附属组的通用虚拟寄存器。 TODO:MachineVerifier是整体的,我们希望执行的某些检查不能整合进去:GlobalISel是独立的库,因此我们不能直接从CodeGen引用它。例如,合法性检查当前在恰当的RegBankSelect/ InstructionSelect中完成。我们可以在检查外添加#ifdef,或添加某种验证器API。 进展与将来的工作 最初的目标是在AArch64上替代FastISel。下一步将是作为优化的ISel替换SelectionDAG。 注意:在遍历GlobalISel时,我们努力避免影响SelectionDAG、FastISel或其他MIR遍的性能。例如,通用虚拟寄存器的类型保存在MachineRegisterInfo中的一张独立表里,在InstructionSelect后被销毁。 FastISel替换 对最初的FastISel替换,我们的目的是在选择失败时,回退到SelectionDAG。 当前,快速流水线的编译时间在FastISel的1.5倍之内。我们对达到1.1/1.2倍以内感到乐观,但鉴于多遍的做法,击败FastISel将是极有挑战性的。不过,支持所有的IR(通过一个完整的合法化器),避免在最坏情形里回退到SelectionDAG,将比SelectionDAG+FastISel,能更好地分摊性能。 注意:我们考虑从不回退到SelectionDAG,相反及早决定给定的函数GlobalISel是否支持。这个决定将基于合法化器的查询。我们因为两个原因放弃回退:1)在IR输入上,我们需要模拟IRTranslator;2)对没有预见的失败变得健壮,并使得迭代改进成为可能。 |
事实上,SelectionDAG是在SDNode上执行选择,生成MachineSDNode对象,在这些对象上进行若干处理后,再转换为MachineInstr对象。GlobIalISel则工作在MachineInstr上(严格地说,这个系统也工作在SDNode上,不过先通过IRTranslator将SDNode翻译为MachineInstr),执行MachineInstr到MachineInstr的选择。
后面我们可以看到,GlobalISel与SelectionDAG共用指令、目标机器的TD描述和处理,它们都是从PatternToMatch对象开始构建关键的匹配表,它们匹配表的构造也是类似的。因为GlobalISel在MachineFunction、MachineBasicBlock及MachineInstr上工作,可以使用它们的方法对MachineInstr进行处理,这提高了代码的重用性,也加速了指令的处理。
3.11.1.1. 指令的定义
在.td文件层面,GlobalISel使用的指令定义的基类是StandardPseudoInstruction(Target.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)的主要内容,如果未能解决你的问题,请参考以下文章