首页 > 其他分享 >LLVM芯片编译器实践示例

LLVM芯片编译器实践示例

时间:2024-03-22 13:12:03浏览次数:29  
标签:LLVM 示例 编译器 指令 寄存器 cpp td def

LLVM芯片编译器实践示例 7.1编译器基本概念 7.1.1AI编译器绪论 芯片是一个硬件,接收的是二进制的指令,要想让自己的编程语言执行编程指令,就需要一个编译器。 这个部分的重要程度丝毫不亚于芯片本身。最近国内很多公司在做AI芯片,经常出现芯片很快就做出来了,但芯片受限于编译器无法发挥最大能效的窘境。总之,了解编译器还是很重要的。 如何用LLVM做一个最简单的编译器。万变不离其宗,其他复杂的编译器可以从这个例子上拓展。 本小节主要介绍基础知识,不需要了解细节,但是对编译器整体如何工作的要有概念。 7.1.2 LLVM的模块化编译器框架[b1] [w2]  首先一个问题要搞明白,为什么要用LLVM? LLVM的是什么?         LLVM提供了一个模块化的编译器框架,让程序员可以绕开烦琐的编译原理,快速实现一个可以运行的编译器。 常见的结构如图7.1所示。    图7.1. LLVM提供了一个模块化的编译器框架 [b3] [w4] [w5] 主要由三个部分组成。 1)前端:将高级语言(如C或者其他语言)代码转换[b6] [w7] 成LLVM定义的中间表达方式LLVM IR。例如非常有名的Clang, 就是一个将C/C++代码转换为LLVM IR[b8] [w9] 的前端。 2)中间表示:中端主要是对LLVM IR本身进行一下优化,输入是LLVM, 输出还是LLVM,主要是消除无用代码等工作,一般来讲这个部分是不需要动的,可以不管他。 3)后端:后端输入是LLVM IR,输出是机器码。通常说的编译器应该主要是指这个部分。大部分优化都从这个地方实现。 至此,LLVM架构的模块化应该说的比较清楚了。很大的一个特点是隔离了前后端。 如果想支持一个新语言,就重新实现一个前端,例如华为仓颉就有自己的前端来替换Clang。 如果想支持一个新硬件,那就重行实现一个后端,可以正确的把LLVM IR映射到自己的芯片。 接下来大致讲讲前后端的流程。 7.1.3前端在干什么 如图7.2所示,以Clang举例,前端主要实现四件事。   图7.2Clang前端四件事 经过词法分析、语法分析、语义分析、LLVM IR生产,最终将C++转化成后端认可的LLVM IR。 1)词法分析:一一取出程序中所有的词汇[b10] [w11] ,遇到不认识的字符就报错。例如将a=b+c 拆成a,= ,b ,+, c; 2)语法分析:将语法提取出来,例如,随手写了个a+b=c, 明显不符合语法规则,直接报错[b12] [w13] ; 3)语义分析:分析一下写的代码实际含义是不是正确,例如a=b+c, a,b,c有没有定义,类型是不是正确的; 4)LLVM IR生成:经过上述三步,将写的代码转化成树状描述(抽象语法树),然后再转化成IR定义的IR即可。 举个直观的例子,写的C++程序。 // add.cpp
int add(int a, int b) { return a + b;
} 这里介绍一下生成的LLVM IR。 这里不需要看懂每个细节,知道LLVM IR类似汇编语言就[b14] [w15] 行了,专业的形式称为SSA(Static Single Assignment 静态单一分配)。 ; ModuleID = 'add.cpp'
source_filename = "add.cpp"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"
; Function Attrs: noinlinenounwindoptnonesspuwtable
define i32 @_Z3addii(i32, i32) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
store i32 %0, i32* %3, align 4
store i32 %1, i32* %4, align 4
%5 = load i32, i32* %3, align 4
%6 = load i32, i32* %4, align 4
%7 = add nsw i32 %5, %6
ret i32 %7
} 7.1.4 后端在干什么 后端把LLVM转换成真正的汇编(或者机器码)。主要的流程如图7.3所示。这个要重点讲一下,因为后续就是要实现一个后端支持一个新的芯片。   图7.3后端把LLVM转换成真正的汇编(或者机器码) 7.1.5 DAG下译[b16] [w17]  这个[b18] [w19] 主要负责将LLVM IR转换为有向无环图,便于后续利用图算法优化。 例如,如图7.4所示,将下面的LLVM IR 转换成图,每个节点是一个指令。 %mul = mul i32%a, %a %mul = mul i32%a, %a [b20] %add = add nswi32 %mul4, %mul ret i32 %add     图7.4将LLVM IR 转换成图,每个节点是一个指令 7.1.6 DAG合法化 DAG图合法化,DAG图都是LLVM IR指令,但实际上LLVM IR指令不可能被芯片全部支持,这个步骤就是替换这些不合法的指令。 1. 指令选择 这个步骤将LLVM IR转换成机器支持的机器DAG。 如图7.5所示,将store指令[b21] [w22] 换成机器认可的st[b23] [w24] 指令, 将16位的寄存器转向32位。一切向机器指令靠拢。   图7.5将store指令换成机器认可的st, 将16位的寄存器转向32位 2. 调度 这个步骤主要是调整指令顺序的,从有向无环图再展开成顺序的指令。 例如,如图7.6所示,把下面的指令调成这样的。   图7.6从有向无环图展开成顺序的指令 把%C的存储提前一些[b25] [w26] 并行进行,因为下一条ld[b27] [w28] 指令要用C语言表示。 3. 基于SSA的机器码优化 主要是做一些公共表达式合并/去除的操作。 4.寄存器分配[b29] [w30]  这一步就要分配寄存器了。也许大家认为[b31] [w32] 寄存器其实是可以无限用的,但实际硬件的寄存器有限的。所以得考虑寄存器数量与寄存器值的生命周期,将虚拟的寄存器替换成实际的寄存器。这个一般会用到图着色等算法,很复杂,好在LLVM都实现好了,不用再重复造轮子。 例如一个芯片,有32个可用的寄存器,如果函数使用到了64个[b33] [w34] 寄存器,剩下的就只能压入堆栈或者处于等待状态[b35] [w36] 了。 5. Prologue/Epilogue代码插入 主要是插入函数调用前的指令和函数调用后的指令,即主[b37] [w38] 要是调用前把参数存下来,调用后把结果写到固定的寄存器里。 6. 窥视孔优化 这个步骤主要是对代码进行最后一次优化[b39] [w40] ,比如把x*2换成x<1[b41] [w42] 方法。 再比如图7.7所示这样:   图7.7将两个32bit的存储[b43] [w44] 过程换成一个64bit的存储 将两个32bit的存储换成一个64bit的存储。 7. 代码发布[b45] [w46]  最后一步显然,将上述优化好的中间代码转换成真正需要的汇编,由汇编器翻译成机器码。 7.1.7 小结 本小节介绍了编译器的基本概念,以及编译过程中的大部分流程。[b47] [w48] 下一小节开始介绍如何用LLVM快捷的实现流程。LLVM的精髓就在于,不必对每一个步骤内部如何实现的彻底了解细节。只需要知道有这个LLVM就能很快攒出编译器。 7.2 从无到有开发 7.2.1 不必从头开始开发 经过了上一小节对编译基础知识的介绍,已经明白了编译器的基本步骤。如果以前对编译没概念建议简单先看看。 那么当自己做了一个芯片,如何利用LLVM从无到有的写自己的编译器呢? 现在开始回答这个问题。 由于LLVM是个开源框架,没必要从头开始。只需要确定在LLVM的框架下添加什么内容即可。这里先看个整体。 7.2.2需要添加的文件类型 LLVM基于C++写的,所以,首先,肯定要添加一堆的.cpp和.h文件。 其次,为了进一步提高编码效率,LLVM其实提供了一套目标定义(target definition)接口(td文件,td的名字就这么来的)。 写td文件,LLVM里有个组件称为tablegen,tablegen读取写的td文件自动帮转成对应的cpp文件,避免所有的cpp都要自己写。 如图7.8所示,大致的原理是这样的:   图7.8读取td文件自动转成对应的cpp文件 现在只需要明白,需要写两种[b49] [w50] 文件。 1)td文件:与架构组件相关的内容,如寄存器等,会在.td文件中编写[b51] [w52] 。 2).cpp和.h文件:其他控制性的、调用性的, 以及不好用.td文件自动生成的,就直接用.cpp文件编写。(所以若C++不熟,阅读或者写代码有难度,不妨找一本C++ PREMIER先学学)。 7.2.3 从文件角度先看整体[b53] [w54] 框架 完成一个简单的后端需要添加多少文件? 现在先看一下,做一个最简单的编译器需要添加什么内容。先看个整体,里面有什么内容。 现在先给后端起个名字,且名字前缀为xxx[b55] [w56] 。xxx能够较为明显的区分写的内容和LLVM自带的内容。 如图7.9所示就是一个简单的LLVM后端需要写的所有文件。不要看到这个地方就被劝退了,其实有些文件内容不多的,一共也就几千行代码。   图7.9一个简单的LLVM后端需要写的所有文件 下面列出了按功能分类的各个文件是如何用的。这里主要看看要写几个方面的内容。 1.芯片总体架构 xxx.h:定义顶层的类。 xx[b57] [w58] x.td:所有td文件的入口,整体芯片各种特性的开关。 xxxTargetMachine.cpp(.h)[b59] [w60] :目标芯片的定义,生成TargetMachine对象。 xxxMCTargetDesc.cpp/h:定义了xxx的各种信息接口。 xxxBasedInfo.h:定义了芯片常见的特性以及指令类型。 xxxTargetInfo.cpp:将target注册到LLVM系统里。 xxxSubtarget.cpp/h:芯片子系列的定义。 2.寄存器描述 xxxRegisterInfo.cpp/h:寄存器信息的底层。 xxxSERegisterInfo.cpp/h:寄存器相关的具体实现。 xxxRegisterInfo.td:具体寄存器的定义。 3.指令相关 1)指令描述 xxxAnalyzeImmediate.cpp/h:处理立即数指令。 xxxInstFormats.td:指令类型定义,I,J等类型。 xxxInstInfo.cpp/h:指令信息的顶层。 xxxSEInstInfo.cpp/h:指令信息的具体实现。 xxxInstInfo.td:各条指令的描述,逐条写。 2)指令处理 第一,LLVM IR 到LLVM DAG: xxxISelLowering.cpp/h xxxSEISelLowering.cpp/h 第二,LLVM DAG 到Machine DAG: xxxISESelDAGToDag.cpp/h xxxISelDAGToDag.cpp/h 3)指令调度 xxxSchedule.td:指令调度需要的信息。 4.堆栈管理 xxxFrameLowering.cpp/h:堆栈的顶层。 xxxSEFrameLowering.td:具体的堆栈操作,包括压栈退栈等操作。 5.函数管理 xxxCallingConv.td:处理函数返回值存储的问题。 xxxMachineFunctionInfo.cpp/h:函数处理顶层。 6.汇编及输出 xxx[b61] [w62]  ASMPrinter.cpp/h:汇编输出顶层。 xxxInstPrinter.cpp/h:实现指令部分的汇编打印。 xxxMCInstLowering.cpp/h:指令内部表示到汇编映射。 xxxSEMCInstLower.cpp/h:指令映射的具体实现。 xxxTargetObjectFile.cpp/h:定义了ELF文件相关内容。 xxxMCAsmInfo.cpp/h:定义一些打印ASM需要的格式信息。 划重点,一共需要写六个方面! 1)芯片总体的架构。 2)寄存器的描述。 3)指令相关的描述。 4)堆栈的管理。 5)函数的管理。 6)汇编和其他输出的管理。 上面实现了最基本的功能,运用熟练以后还可以自己添加其他功能。不需要搞清楚每个文件具体是干什么的,后续返回来看就能明白,会逐一来细讲。 7.2.4从类继承与派生角度看整体[b63] [w64] 框架 上面以文件角度看可能对于熟悉C++的来讲还不够脉络清晰,换个角度看世界。 既然以类角度看,那就需要来个抓手。如图7.10所示,在编译器中,这个抓手其实是xxxSubtarget类。   图7.10在编译器中 xxxSubtarget类抓手 有了这个类,大部分的资源都能通过指针访问到。同时,其他类也通过指向Subtarget的指针获得了访问其他信息的接口。 总而言之, Subtarget类是一个接口类。实现了资源的互通调用。这样其他类的关系就有了方向了。一般来讲,要先在代码中找到subtarget的指针[b65] [w66] ,然后通过subtarget访问其他[b67] [w68] 模块。 td文件通过tablegen生成对应的xxx[b69] [w70] Geninfo类[b71] [w72] ,然后通过派生合入自己写的类。最后Subtarget通过指针访问之。 划重点: 1)Subtarget类是接口类。 2)td文件通过tablegen生成[b73] [w74] 类,然后通过派生合入写的类。 实际上是解决LLVM如何用起来这个问题的开始。觉得开源项目要想做的快,一定是要先看森林后看树叶的。否则很容易陷入开源代码的汪洋大海中毫无方向。这里从两个角度介绍了宏观的框架,然后再转到最后完成的目标[b75] [w76] ,属于介绍LLVM的整体。从下一小节开始,逐个方向介绍如何一步步组合起来编译器。 7.3 芯片的整体架构部分 介绍了从宏观上看LLVM需要补充什么内容,从开始逐个讲各部分需要的代码。 首先介绍芯片的整体架构部分。这一部分可能稍微有点儿枯燥,没有讲寄存器和指令那么的清晰明了,但它确实是LLVM编译器的入口,首先就要完成这一部分代码, 所以需要耐心看完。 [b77] [w78] 7.3.2 xxx.h类型文件 这个文件是要完成的第一个文件,其实就是声明了两个类,方便后续引用。 namespace llvm{   class xxxTargetMachine;   class FunctionPass; } 后续可以把全局的宏定义写到这个里面。 7.3.3 xxx.td类型文件 接下来写xxx.td。emmm 这个文件主要定义整体架构层面的内容。 1.定义一些子目标特征 例如定义两个特征, 分别支持slt和cmp指令。 def FeatureCmp: SubtargetFeature<"cmp", "HasCmp", "true", S                                  "Enble 'cmp' instructions.">; def FeatureSlt: SubtargetFeature<"slt", "HasSlt", "true",                                  "Enble 'slt' instructions.">; 然后为了简便[b79] [w80] ,定义这些特征的集合。 def Feature32II: SubtargetFeature<"xxx32II", "xxxArchVersion",                                   "xxx32II", "xxx32II ISA Support",                                   [FeatureCmp, FeatureSlt]>; 例如定义xxx32II特征,包含cmp和slt[b81] [w82] 。 这部分其实是利用tablegen来完成的,基类是SubtargetFeature。后续可以确定某个Subtarget类带[b83] [w84] 不带某个特征。 至于这个基类SubtargetFeature, [b85] [w86] 显然用的是LLVM提供的接口。 class SubtargetFeature<string n, string a, string v, string d,                        list<SubtargetFeature> i = [ ]> {   string Name = n;   string Attributes = a;   string Value = v;   string Desc =d;   list<SubtargetFeature> Implies = i; } 没有太多内容,就是定义了一下名字,描述之类的。 2. 定义一些处理器 然后就是定义几个。这个处理器带了某个特性。 class Proc<string Name, list<SubtargetFeature> Features>   : Processor<Name, xxxGenericItineraries, Features>; def: Proc<"xxx32I", [Featuresxxx32I]>; def: Proc<"xxx32II", [Featuresxxx32II]>; 例如上面就定义了两个Subtarget处理器,一个含有Feature[b87] [w88] xxx32I特性,另一个含有Feature[b89] [w90] xxx32II特征。 这里把架构定义出来。 def xxx: Target {   // 按照以前的方法定义xxxInstrInfo: InstrInfo   let InstructionSet = xxxInstrInfo; } 7.3.4 xxxTargetMachine.cpp/h类型文件 1.定义target类 这部分h文件描述TargetMachine类。这个类包含了对各个subtarget类的映射[b91] [w92] 。 从LLVMTargetMachine里继承出xxxTargetMachine类。 class xxxTargetMachine: public LLVMTargetMachine {   bool isLittle;   std::unique ptr<TargetLoweringObjectFile> TLOF; xxxSubtargetDefaultSubtarget;   mutable StringMap<std::unique_ptr<xxxSuntarget>>SuntargetMap; 其实可以理解为对Subtarget的管理,提供了接口,可以方便地拿出需要的s[b93] [w94] ubtarget。 当然,为了使用方便,还能用xxxTargetMachine,可以分别派生出大、小端的[b95] [w96] 类。 classxxxTargetMachine: public xxxTargetMachine 2. 注册target类 在C文件里,比较重要的是需要调用一个LLVM的库函数,把写的类注册给LLVM。 extern "C" void LLVMInitialxxxTarget {   // 注册目标   // 小端目标机器 [b97] [w98]   RegisterTargetMachine<xxxTargetMachine> Y(getThexxxTarget( )); } 3. 实现Pass的配置[b99] [w100]  此处在C文件里直接实现xxxPassConfig类,继承自TargetPassConfig, 用来配置Target的Pass。(什么是Pass: 就是一个处理操作,例如程序选择就是一个Pass) class xxxPassConfig: public TargetPassConfig{ public: TargetPassConfig(TM, PM) { }; bool addInstSelector( ) override { addPass(createxxxSEISelDAG(getxxxTargetMachine( ), detOptarget);    return false; } 例如上面的代码就是重载了addInstSelector, 注册了自己写的指令选择器。 7.3.5 xxxMCTargetDesc类文件[b101] [w102]  这部分代码正式生成Target对象,并把各种类都注册给LLVM。具体注册了些什么,可参考cpp文件。 目标对象主要[b103] [w104] 在LLVMInitializexxxTarget机器码里: extern "C" void LLVMInitializexxxTarget(){   Target &ThexxxTarget = getThexxxTarget();   //Target &ThexxxTarget = getThexxxTarget();[b105] [w106]    for(Target *T: {&ThexxxTarget}){   // 注册机器汇编代码信息 RegisterMCAsmInfoFn X(*T, createxxxMCAsmInfo);   //注册机器码指令信息TargetRegistry::RegisterMCInstrInfo(*T, createxxxMCInstrInfo);   //注册机器码注册信息 TargetRegistry::RegisterMCRegInfo(*T, createxxxMCRegisterInfo);   //注册机器码子目标信息 TargetRegistry::RegisterMCSubtaregetInfo(*T, createxxxMCSubtargetInfo);   //注册机器码指令分析器 TargetRegistry::RegisterMCInstrAnalysis(*T, createxxxMCInstrAnalysis);   //注册机器码指令输出 TargetRegistry::RegisterMCInstPrinter(*T, createxxxMCInstrInfo); } 把Target, Asminfo, Inst, Reg, Subtarget等类全部注册给LLVM。 7.3.6 xxxbaseInfo类型文件 这是个.h文件,倒是比较简单。定义了两个枚举类型,一个是TOF(Target Operand Flag),即目标操作数标志 enumTOF { MO_NO_FLAG, } 另一个是指令编码类型。 [b107] [w108] enum { // 伪指令:这表示一条伪指令或尚未实现的指令。代码生成是非法的,但在中间实// 现阶段是可以容忍的  Pseudo = 0,   // FrmR:这张表格是用于R格式的说明 FrmR = 1,   // FrmI:这张表格是用来说明格式I的 FrmI = 2,   // FrmJ: This form is for instructions ofr the format J. FrmJ = 3, } 现在没什么其他内容了,比较简单。 7.3.7 xxxTargetInfo类型文件 这个文件简单,主要就是实现一个函数getTheTarget[b109] [w110] ,获取到Target类。 Target &llvm::getThexxxTarget(){   static Target ThexxxTarget;   return ThexxxTarget; } 7.3.8 xxxSubtarget类型文件 emmm 然后来到了最重要的一个类。这个类前面讲过,这是个接口类,主要定义了一系列Subtarget[b111] [w112] 对外的接口。 这个地方Subtarget[b113] [w114] 有哪些在td文件中定义过[b115] [w116] ,这里创建一个枚举类型[b117] [w118] , 显然,td文件要与 .h文件对上。 enumxxxArchEnum{   xxx32I,   xxx32II, }; 所以可以看出,td文件和cpp/.h文件是要配合使用的,两个剥离开了理解是非常痛苦的一件事儿。这里类里定义了一堆借口。 const xxxInstrInfo *getInstrInfo()      const overide { return InstrInfo.get(); } const TargetFrameLowering *getFrameLowering()      const override { return FrameLowering.get(); } const xxxRegisterInfo *getRegisterInfo()      const override { return &InstrInfo->getRegisterInfo(); } const xxxTargetLowering *getFrameLowering()      const override { return TLInfo.get(); } 例如上面这样的,返回的全是各类的指针。 7.3.9 几个容易混淆的概念 上面这部分代码重点已经讲完了。可能看到这里有几个点有疑惑,这里需要解释一下。 Target和Subtarget[b119] [w120] 的区别:Target说的是一类芯片,例如ARM是一个Target, SubTarget是具体的一个芯片,例如ARM的M3。不同的芯片有不同的特性[b121] [w122] [b123] [w124] ,有些支持slt指令[b125] [w126] , 有些不支持。把这些特性总结成特征, 然后定义各种Subtarget[b127] [w128] 即可。否则岂不是要写若干编译器吗? Target和TargetMachine区别:Target是信息层面的类,比如Target是什么,有哪些特征?TargetMachine是操作层面的类,用于管理subtarget[b129] [w130] 的。 7.3.10 小结 从LLVM森林看到LLVM树木的内容。介绍了如何在LLVM上定义出架构,方便后续添加枝叶进去。可能有点晦涩,可对照代码来读。 下一小节介绍如何添加寄存器信息进去。 7.4寄存器信息 7.4.1 寄存器概述 本小节介绍寄存器。处理器就是通过指令对寄存器里的值进行各种操作[b131] [w132] 。 先来看看,要在LLVM中描述寄存器需要哪些内容。 寄存器描述: xxxRegisterInfo.cpp/h:寄存器信息的底层。 xxxSERegisterInfo.cpp/h:寄存器相关的具体实现。 xxxRegisterInfo.td:具体寄存器的定义。 7.4.2 xxxRegisterinfo.td 首先介绍td[b133] [w134] 格式的文件。这个才是描述寄存器的主力。td[b135] [w136] 文件最终会通过tablegen生成xxxGenRegisterInfo, 供后续合入到Registerinfo类中去。 这里面还是比较简单的,首先继承一个寄存器基类,生成16bit[b137] [w138] 参数。 class xxxReg<bits<16> Enc, string n>: Register<n> {    // 对于CMakeLists.txt中的tablegenablegen( ... -gen-emitter)    let HWEncoding = Enc;    let Namespace = "xxx"; } 然后派生出两种类型的寄存器:通用寄存器和C0寄存器。[b139] [w140]  // 两种寄存器类型 class xxxGPRReg<bits<16>Enc, string n>: xxx<Enc, n>; class xxxC0Reg<bits<16>Enc, string n>: xxx<Enc, n>; 接下来定义所有寄存器[b141] [w142] 。 //定义全部寄存器 let Namespace = "xxx" in {   //@General Purpose Registers   def ZERO: xxxGPRReg<0, "zero">, DwarfRegNum<[0]>;   def AT: xxxGPRReg<1, "1">, DwarfRegNum<[1]>;   def V0: xxxGPRReg<2, "2">, DwarfRegNum<[2]>;   def V1: xxxGPRReg<3, "3">, DwarfRegNum<[3]>;   def A0: xxxGPRReg<4, "4">, DwarfRegNum<[4]>;   def A1: xxxGPRReg<5, "5">, DwarfRegNum<[5]>;   def T9: xxxGPRReg<6, "6">, DwarfRegNum<[6]>;   def T0: xxxGPRReg<7, "7">, DwarfRegNum<[7]>;   def T1: xxxGPRReg<8, "8">, DwarfRegNum<[8]>;   def S0: xxxGPRReg<9, "9">, DwarfRegNum<[9]>;   def S1: xxxGPRReg<10, "10">, DwarfRegNum<[10]>;   def GP: xxxGPRReg<11, "11">, DwarfRegNum<[11]>;   def FP: xxxGPRReg<12, "12">, DwarfRegNum<[12]>;   def SP: xxxGPRReg<13, "13">, DwarfRegNum<[13]>;   def LR: xxxGPRReg<14, "14">, DwarfRegNum<[14]>;   def SW: xxxGPRReg<15, "15">, DwarfRegNum<[15]>; } 再然后定义寄存器组。 //def Register Groups def CPURegs: RegisterClass<"xxx", [i32], 32, (add   // Reserved   ZERO, AT,   // Return Values and Arguments   V0, V1, A0, A1,   // Not preserved across procedure calls   T9, T0, T1,   // Callee save   S0, S1,   // Reserved   GP, FP,   SP, LR, SW)>;   // @Status Registers class def SR: RegisterClass<"xxx", [i32], 32, (add SW)>; // @Co-processor 0 Registers class def C0Regs: RegisterClass<"xxx", [i32], 32, (add PC, EPC)>; def GPROt: RegisterClass<"xxx", [i32], 32, (add(sub CPURegs, SW)>; def C0Regs: RegisterClass<"xxx", [i32], 32, (add HI, LO)>; 例如C0 寄存器组是一个32位宽的寄存器组,是3[b143] [w144] 2位对齐的,包含了PC和EPC。 寄存器组相当于把单个寄存器变成了二维数组,可以拼起来访问。 寄存器中的.td[b145] [w146] 文件里目前就含这么多内容,这是不是也不复杂? 7.4.3 xxxRegisterInfo类型文件 很显然,这个[b147] [w148] xxxRegisterInfo是xxx中寄存器的基类,td文件[b149] [w150] 通过xxxGenRegisterInfo合入进来。 classxxxRegisterInfo: publicxxxGenRegisterInfo{ 然后定义了一堆寄存器操作的特殊函数。可以选择性的重载一些。具体能重载什么,看官方文档,一大堆。 例如,重载一个位反序的函数。[b151] [w152]  LLVM: llvm::TargetRegisterInfo Class Reference​llvm.org/doxygen/classllvm_1_1TargetRegisterInfo.html#a8681f09dd6db9839e0cdf1155312c451 BitVectorgetReservedRegs(constMachineFunction&MF) constoverride; } 其他函数也大致如此,不再赘述。 需要说明的是,在这个TargetRegisterInfo类[b153] [w154] 里写了一个eliminateFrameIndex的成员函数。这部分到后续堆栈管理时一起讲。 7.4.4xxxSERegisterinfo类型文件 这个文件非常简单。主要就是继承上面的类,多实现了个成员函数intRegClass。 class xxxxRegisterInfo: public xxxRegisterInfo {   public: xxxRegisterInfo(const xxxSubtarget&Suntarget);   const TargetRegisterClass *intRegClass(unsigned Size) const override; }; } // end namespace llvm 这里[b155] [w156] 没有什么实质性的内容,类程序定义用来[b157] [w158] 追踪注册类[b159] [w160] 的。 本小节比较简单以及短小。讲了LLVM中的寄存器描述,可以看到这一部分的c++描述其实不多,大多是.td文件完不成的任务采用C ++去做,大多数的寄存器信息都记录在了.td文件中。搞懂了.td文件基本就无障碍了。下一小节来讲指令的描述。 7.5指令描述的.td文[b161] [w162] 件 7.5.1 指令相关概述 本小节来到了指令相关的部分,这部分内容是编译器最重要的信息。 先来看看这部分主要有哪些内容。 [b163] [w164] 由于指令描述部分太过重要,因此分7.5节和7.6节进行介绍,本节先介绍指令描[b165] [w166] 述的.td文件。 7.5.1 xxxInstFormats.td类型文件 1. 指令类型的例子 首先从简单的开始介绍,既然说指令,那么常见的指令类型应该有所了解? 比如,如图7.11所示,MIPS或者RISC的指令,对编译器来讲,最重要的信息是一个指令里有几个操作数。   图7.11 指令类型与操作数 2. Formats文件定义 首先把类定义出来。 class Format<bits<4> val> {    bits<4> Value = val; } def Pseudo:    Format<0>; def FrmA:      Format<1>; def FrmL:      Format<2>; def FrmJ:      Format<3>; def FromOther: Format<4>; // Instruction w/ aCustomformat 然后定义指令中通用的基类。 // Generic xxx Format class xxxInst<dag outs, dag ins, string asmStr, list<dag> pattern, InstrItinClassitin, Format f>: Instruction {     // Inst and Size: for tablegen( ... -gen-emitter) and     // tablegen( ... -gen-disassembler) in CMakeLists.txt   field bits<32> Inst;   Format Form = f;   let Namespace = "xxx";   let Size = 4;   bits<8>Opcode = 0;   //Top 8 bits are the opcode field   let Inst{31-24} = Opcode;   let OutOperandList = Outs;   let InOperandList = ins;   let AsmString = asmStr;   let Pattern = pattern;   let Itinerary = itin;   //Attributes specific to xxx instructions   bits<4>FormBits = Form.Value;   let TSFlags{3-0} FormBits;   let DecoderNamespace = "xxx";   field bits<32>SoftFail = 0; } 上面直接继承了LLVM的指令基类,指定参数。还顺手把opcode的位宽给指定了一下。 然后就是几个类的定义,都继承上面这个基类。 class FA<bits<8> op, dag outs, dag ins, string asmStr,          list<dag> pattern, InstrItinClassitin>   :xxxInst<outs, ins, asmStr, pattern, itin, FrmA> {   bits<4> ra;   bits<4> rb;   bits<4> rc;   bits<12>shamt;   let Opcode = op;   let Inst{23-20} = ra;   let Inst{19-16} = rb;   let Inst{15-12} = rc;   let Inst{11-0}  = shamt; } A型的如上面所示,L,J型类似,就是指定一下不同比特代表的是什么。 另外,还定义了伪指令的类,这些指令不会出现在最后的机器码里,仅仅用在中间处理上。 // xxx Pseudo Instructions Format class xxxPseudo<dag outs, dag ins, string asmstr, list<dag> pattern, InstrItinClassitin = IIPseudo> : xxxInst<outs, ins, asmstr, pattern, itin, Pseudo>{ let isCodeGenOnly = 1; let isPseudo = 1; } 7.5.2 xxxInstInfo.td类型文件 上面定义了指令类型,具体支持的指令在这个文件里来定义。 1. 定义返回值结点[b167] [w168]  首先,定义一个返回值结点。在DAG中,指令就是结点。 // 返回 def xxxRet: SDNode<” xxxISD:: Ret”, SDTNode,           [SDNPHasChain, SDNPOptInGlue, SDNPVariadic]>; 可以看到,定义一个返回结点,名字是xxxISD:[b169] [w170] :Ret,SDTNone指的是无类型要求,后面三个是参数。 例如SDNPVariadic表示允许可变参数。然后把这个结点的类型指定一下。 def SDT_xxxRet: SDTypeProfile<0, 1, [SDTCisInt<0>]>; 表示这个结点有0个结果(都返回了,显然没有输出结果),1个操作数,这个操作数类型是整数的(SDTCisInt<0>)。 2. 定义操作数 然后定义一堆操作数。 // Signed Operand def simm16: Operand<132>{     let DecoderMethod = "DecodeSimm16"; }   def shamt: Operand<132>;   def uimm16: Operand<i32>{   let PrintMethod = "printImsignedImm"; }   // Address operand def mem: Operand<iPTR> {   let PrintMethod = "printImsignedImm";   let MIOperandInfo = (ops CPURegs, simm16);   let EncoderMethod = "getMemEncodeing"; } 比如定义为有符号数、无符号数[b171] [w172] 等。同时,制定了各操作数的打印方式。let PrintMethod = "xx" 后面的xx在cpp文件里实现的打印方法。 同时,此处还定义了一下转换函数。 // Transformation Function: get the higher 16 bits. def HI16: SDNodeXForm<imm, [{   return getIMM(N, (N->getZExtValue( ) >> 16) & 0xffff); }]>; 例如,取立即数的高16位。这种内容.td文件[b173] [w174] 没有语法描述,所以干脆就用c++函数写出来了。 3. 定义指令的具体形式 // Arithmetic and logical instructions with 3 register operands class ArithLogicR<bits<8> op, string instr_asm, SDNodeOpNode, InstrItinClassitin, RegisterClass RC, bit isComm = 0> : FA<op, (outs GPROut: $ra), (ins RC: $rb, RC: $rc), !strconcat(instr_asm, “\t$ra, $rb, $rc”), [(set GPROut: $ra, (OpNode RC: $rb, RC: $rc))], itin>{ let shamt = 0; let isCommutable = isComm; let isReMaterializable = 1; } 通过上面定义的格式, 派生出指令的具体形式。 例如通过FA,制定其打印形式 \t$ra, $rb, $rc。在DAG图上表现为输出为ra寄存器,输入为rb,rc寄存器。 7.5.3 按个定义指令 然后就该按个把指令定义出来了。 defADDu: ArithLogicR<0x11, addu, add, IIAlu, CPURegs, 1>; defADDu: ArithLogicR<0x12, subu, sub, IIAlu, CPURegs>; 例如上面形式,定义了一个ADDu,采用的是ArithLogicR指令,opcode是0x11, 名字是addu, SDNODE是add。将指令绑定到硬件的ALU单元, 操作的寄存器组是CPURegs, ADDu是可执行的,SUBu最后需要转化一下。 有了这些信息,编译器基本就可以编译指令了。 7.5.4 定义指令的自动转换 包括了自动将小的立即数操作转换为加法等(从L型转换为了A型,不一定发生,但是要告诉编译器能转换)。 // Immediates def: Pat<i32 immSExt16: $in), (ADDiu, ZERO, imm: $in)>; 还包括了一些指令的别名。 def: xxxInstAlias<”move $dst, $src”,(ADDuGPROut: $dst, GPROut: $src, ZERO), 1>; 例如,move实际上实现的时候,这是使用ADDu+ 0来完成。 7.5.5 小结 本节重点讲了描述指令的.td文件,该文件的编写在编译器中占了很大的工作量,但其实也不难理解, 因为它给出了最基础的用法。[b175] [w176]  7.6 描述指令的.cpp文件[b177] [w178]  7.6.1指令描述 xxxAnalyzeImmediate.cpp/h:处理立即数指令。 xxxInstFormats.td:指令类型定义,I,J等类型。 xxx.InstInfo.cpp/h:指令信息的顶层。 xxxSEInstInfo.cpp/h:指令信息的具体实现。 xxxInstInfo.td:各条指令的描述,逐条写。 现在来接着讲,指令的.cpp文件[b179] [w180] 描述里面有什么内容。 7.6.2 xxxInstr[b181] [w182] Info.cpp/.h[b183] [w184] 类型文件 这个头文件[b185] [w186] 主要是指令类。由于大部分内容都在td文件里定义里,这个里面就简单的几个函数定义。 void storeRegToStackSlot(MachineBasicBlock&MBB, MachineBasicBlock:: iterator MBBI,                        Register SrcReg, bool iskill, int FrameIndex,                        const TargetRegisterClass *RC,                        const TargetRegisterInfo *TRI)                        const override{ storeRegToStack(MBB, MBBI, SrcReg, isKill, FrameIndex, RC, TRI, 0); }   void loadRegFromStackSlot(MachineBasicBlock&MBB, MachineBasicBlock:: iterator MBBI,                         Register DestReg, int FrameIndex,                         const TargetRegisterClass *RC,                         const TargetRegisterInfo *TRI)                         const override{ loadRegToStack(MBB, MBBI, DestReg, FrameIndex, RC, TRI, 0); } void storeRegToStackStack(MachineBasicBlock&MBB, MachineBasicBlock:: iterator MI,                           Register SrcReg, bool iskill, int FrameIndex,                           const TargetRegisterClass *RC,                           const TargetRegisterInfo *TRI,                           int64_t offset)                           const = 0; void loadRegFromStackSlot(MachineBasicBlock&MBB, MachineBasicBlock:: iterator MI,                           Register DestReg, int FrameIndex,                           const TargetRegisterClass *RC,                           const TargetRegisterInfo *TRI,                           int64_t offset)                           const = 0; 这几个定义主要[b187] [w188] 包括对寄存器存取堆栈[b189] [w190] 的操作,此处定义成了纯虚函数,后续在SE(SEInstInfo.cpp)中实现,其他所有内容已经都在td文件里描述过了。 7.6.3 xxxSEInstInfo.cpp/h类型文件 SE中实现了上述几个存取堆栈的操作。 void xxxSEInstInfo:: storeRegToStack(MachineBasicBlock&MBB, MachineBasicBlock::iterator I,                 Register SrcReg, bool isKill, int FI,                 const TargetRegisterClass *RC, const TargetRegisterInfo *TRI,                 int64_t Offset) const { DebugLoc DL; MachineMemOperand *MMO = GetMemOperand(MBB, FI, MachineMemOperand:: MOStore);   unsigned Opc = 0; Opc = xxx::ST;   assert(Opc&& "Register class not handled!"); BuildMI(MBB, I, DL, get(Opc)).addReg(SrcReg, getKillRegState(isKill))      .addFrameIndex(FI).addImm(Offset).addMemOperand(MMO); } 这里直接调用了BuildMI的成员函数addReg, 将SrcReg给存到堆栈里面去。 7.6.4 xxxAnalyzement[b191] [w192] diate.cpp/h类型文件 这个文件里主要是一堆关于立即数的调用函数。具体的函数如下面代码所示。 // AddInstr - 将I添加到SeqLs中的所有指令序列 void AddInstr(InstSeqLs&SeqLs, const &I); // GetInstSeqLsAddiu - 获取结束ADDiu以加载即时Imm的指令序列 void GetInstSeqLsADDiu(uint64_t Imm, Unsigned RemSize, InstSeqLs&SeqLs); // GetInstSeqLsORi - 获取结束ORI以加载即时Imm的指令序列 void GetInstSeqLsORi(uint64_t Imm, Unsigned RemSize, InstSeqLs&SeqLs); // GetInstSeqLsSHL - 获取以SHL结尾的指令序列以加载即时Imm void GetInstSeqLsSHL(uint64_t Imm, Unsigned RemSize, InstSeqLs&SeqLs); // GetInstSeqLsSeqLs - 获取指令序列以加载即时Imm void GetInstSeqLs(uint64_t Imm, Unsigned RemSize, InstSeqLs&SeqLs); // RepalceADDiuSHLWithLUi - 获取指令序列以加载即时Imm void RepalceADDiuSHLWithLUi(InstSeqLs&Seq); // GetShortestSeq - 在SeqLs中查找最短的指令序列,并在Insts中返回 void GetShortestSeq(InstSeqLs&SeqLs, InstSeq&Insts); 可以将一些不支持的操作转换为支持的操作。比如把ADDiu和SLH打包成LUI。处理非常大的立即数是有用。 addiu¥1, $zero, 8; shl$1, $1, 8; addiu$1, $1, 8; addiu$sp,$sp, $1; 比如处理时将上面的指令替换成下面的指令 lui¥1, 32768; addiu$1, $1, 8; addu$sp,$sp, $1; 可以在一定程度上提升运行效率[b193] [w194] 。其实,这些指令主要[b195] [w196] 体现在堆栈[b197] [w198] 非常大,sp指针的跳转比较费事。Sp指令逻辑[b199] [w200] 比较复杂,具体的实现是递归的。 内容就这么多,其实讲得有点粗略,感兴趣的话直接深入代码看。指令这一部分重头戏都在td上,其他cpp文件更多的是给td文件打了个补丁,比如堆栈的操作,立即数的操作等。总之,每个函数也不必全搞懂,知道每个文件都是干什么的就达到基本目标了,后续如果要用的话再详细读代码。    

标签:LLVM,示例,编译器,指令,寄存器,cpp,td,def
From: https://www.cnblogs.com/wujianming-110117/p/18089232

相关文章

  • 阿里巴巴中国站按图搜索1688商品(拍立淘) API接口item_search_img响应示例说明
    阿里巴巴中国站的按图搜索1688商品(拍立淘)API接口item_search_img允许用户通过上传图片来搜索与图片相似的商品。以下是关于该接口响应示例的说明:响应示例概述当调用item_search_img接口并成功上传图片后,接口会返回一个响应,该响应通常包含与上传图片相似的商品信息列表。这些......
  • python 代码练习示例
    判断数字位数##给定一个不超过5位的整数,判定该数的位数,以及依次打印,万位到个位。#接收用户输入的整数num=int(input("请输入一个小于等于5位数的整数:"))#将整数转换为字符串,计算整数的位数num_str=str(num)length=len(num_str)iflength>5:print("输入......
  • 使用概率分析与指示器随机变量解决生日悖论问题及C代码示例
    概率分析与指示器随机变量解决生日悖论问题一、引言二、生日悖论的概率分析三、指示器随机变量解决生日悖论问题四、C代码实现五、结论与启示一、引言在日常生活中,我们经常会遇到一些与概率相关的问题,其中生日悖论就是一个非常有趣且典型的例子。生日悖论指的是在一......
  • 开源一个教学型分库分表示例项目 shardingsphere-jdbc-demo
    在笔者心中,消息队列,缓存,分库分表是高并发解决方案三剑客。分库分表之所以被广泛使用,因为工程相对简单,但分库分表并不仅仅是分片,还是需要考虑如何扩缩容(全量同步、增量同步、数据校验等)。因此笔者做了一个教学型分库分表示例项目,计划将分库分表的技术体系都实际演示一遍。ht......
  • vue2/3 - element表格组件el-table实现懒加载树型(上下级)数据、默认展开和隐藏层级,支
    效果图在vue2、vue3项目开发中,使用element饿了么组件库,实现Table表格组件动态懒加载表格数据,可以决定是否自动展开所有2级或3级,也可以点击加载下级数据,可搭配表格的增删改查,数据变化后自动更新列表不会破坏树状的展开和折叠结构。提供详细示例代码,一键复制运行查看效果,稍......
  • 魔兽世界LUA插件开发与示例
    魔兽世界LUA插件开发1.创建插件1.1创建插件文件夹打开WorldofWarcraft\Interface\AddOns文件下,在该文件夹下创建一个插件名文件夹用来存放插件,如Makubex1.2创建插件文件在该文件夹下创建俩个文件,一个是用来给魔兽世界引入的toc头文件,一个是你自己的lua脚本文......
  • ThreadLocal详解及用法示例
    ThreadLocal概念ThreadLocal 是Java并发包(java.util.concurrent)中提供的一个类,它的主要作用是在多线程环境下为每个线程提供一个独立的变量副本,使得每个线程在访问 ThreadLocal 时获取到的都是自己的私有变量,而不是共享的同一个变量。换句话说,ThreadLocal 能够隔离线程间......
  • gRPC简单示例
    gRPC概述gRPC是一种跨语言的RPC框架,之所以它能跨语言,是因为它基于protobuf描述对象实体和方法,最后通过protobuf编译器生成指定语言的代码。这样,就能通过一套protobuf声明生成多种语言的相同API,对于实现跨语言的RPC通信非常便利,同时也使用protobuf作为通信的序列化协议。如下通......
  • AMD Zen5越来越近了!Linux GCC编译器已支持
    AMD预计会在今年年中左右开始推出下一代Zen5CPU架构产品,首先从移动端开始,然后是桌面端、服务器端,相关支持也正在紧锣密鼓地进行中,尤其是Linux系统下。现在,AMD已经将Zen5微架构加入到了GCC编译器的支持,GCCGit仓库的target设定值为“znver5”,可以赶上GCC4.1稳定版的发布。目前......
  • 速通编译器前端
    编译器前端的概念词法分析及词法分析工具语法分析方法上下文无关文法与左递归的文法与左递归的消除方法递归下降的语法分析方法LL(k)语法分析方法#######first集合#######follow集合LR(k)语法分析方法LALR语法分析方法错误恢复方法语法制导的翻译语法制......