LLVM相关-----学习笔记


前言:为学习LLVM,看了一些博客和LLVM的官方文档。写这篇博客,是为了梳理一下自己看的内容,并记录一些自己的理解。同时,也希望将LLVM的知识点结构化,以此加深自己的理解。目前,本文涵盖的LLVM知识较浅,未来会基于现有的文章结构,以添加章节和内容的方式进行补充、加深。

1 LLVM的简介

1.1 LLVM的概念

LLVM项目是模块化、可重用的编译器以及工具链技术的集合。

(简单说,LLVM是一个编译器。特点:开源、前后端分离、优化模块可重用。)

1.2 LLVM的架构

正常编译器的结构:前端 —— 中间代码优化器 —— 后端。如下图:

(图不是很直观,有空自己画)
在这里插入图片描述
LLVM的结构:前端 —— 中间代码优化器(pass) —— 后端。如下图:
(图不清晰,有空自己画)
在这里插入图片描述

总结:一般的编译器(如,GCC)前端后端耦合在一起,如果需要支持一门新的高级语言或是新的平台,会非常困难。而LLVM将前端和后端分开,中间用LLVM独有的中间代码(LLVM IR)连接。如果需要支持一门新的语言,只需要实现一个新的前端。如果需要支持一个新的平台,只需要实现一个新的后端。另外,LLVM的优化阶段是一个通用的阶段,它针对的是统一的LLVM IR,不论是支持新的编程语言,还是支持新的硬件设备,都不需要对优化阶段做修改。

(这边只是说一下LLVM结构的优势,具体的优化细节在下面pass章节)

2 一般编译器的编译流程

(编译器的细节得看编译原理了。没学过编译原理,简单看了一些博客文章,先填充一下。当然,也可以参考下一章“LLVM的编译流程”。中间代码前的阶段,二者应该差不多)

编译过程主要分为词法分析、语法分析、中间代码生成、目标代码生成(忽略预处理、语义分析、优化等)。

2.1 总体编译流程

编译步骤
(1)词法分析器从16进制的源代码中读取token,并按序保存。

(2)语法分析器将token一个个与(C语言)模板进行匹配,匹配上模板的某个语法(产生式),就可以识别出一个完整的语句,并确定该语句的语法。最后以树型结构(语法树)存储在内存中。

(3)语法树是个二维结构,中间代码是个准一维结构,目标代码是一维结构。语法树到目标代码的转换,本质上是二维结构转换为准一维结构的过程。

(4)选定具体CPU和操作系统后,中间代码就可以转换成目标代码(.s 存着汇编代码)

(5)汇编器将目标代码转换为目标文件(.o/.obj 存着选定的CPU的机器指令)

(6)链接器把一个和多个目标文件(库文件本质上也是目标文件)链接成符合选定操作系统指定格式的可执行文件。

(7)通过操作系统,就可以将可执行文件载入内存,形成运行时的内存结构。

编译流程图
(图丑,有空自己画)
在这里插入图片描述
编译结构图
在这里插入图片描述

2.2 词法分析

概念:词法分析阶段是编译过程的第一个阶段。这个阶段的任务是从左到右逐个字符地读入源程序,即对构成源程序的字符流进行扫描,然后根据构词规则识别单词(也称单词符号或符号)。词法分析程序实现这个任务。词法分析程序可以使用lex等工具自动生成。

正则表达式:用于对字符串的搜索、切割、替换。

正则引擎:DFA、NFA、POSIX NFA

2.3 语法分析

概念:语法分析是编译过程的一个逻辑阶段。语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等。语法分析程序判断源程序在结构上是否正确。源程序的结构由上下文无关文法描述。

一些术语:终结符、非终结符、产生式、左递归、回溯。

2.4 语义分析

概念:语义分析是编译过程的一个逻辑阶段。语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。如一个C程序片断:

int arr[2],b;
b = arr * 10;

源程序的结构是正确的。但语义分析会审查类型并报告错误:不能在表达式中使用一个数组变量,赋值语句的右端和左端的类型不匹配。

2.5 中间代码

(语法树到中间代码的转换,以gcc 4.8.2为例)

转换步骤

(1)语法树

(2)高端gimple(语法树到高端gimple的过程会完成该阶段绝大部分的转换)

(3)低端gimple
变化较小。返回语句的特殊处理。

(4)CFG(Control Flow Graph,控制流图)
该步将gimple语句转换成基本快,基本块中的语句都是顺序执行的。即,导致跳转的语句就是基本块的边界语句(比如,函数调用、return等)。
3种控制流转移的指令:各种跳转指令、call、ret。

(5)SSA(Static Single-Assignment,静态单一变量赋值)
主要变化是变量多了版本号,用于数据优化。

(6)RTL(Register Transfer Language)
第一步,数据转化为RTL。第二步,SSA中的语句转化为RTL。

(7)汇编代码

2.6 汇编与链接

转换步骤

(1)汇编器将目标代码转换为目标文件(.o/.obj 存着选定的CPU的机器指令)

(2)链接器把一个和多个目标文件(库文件本质上也是目标文件)链接成符合选定操作系统指定格式的可执行文件。

(预编译、中代码优化等未写)

3 LLVM的编译流程

3.1 LLVM的编译流程

LLVM的编译流程如下图:

(图丑,有空自己画)
在这里插入图片描述

3.2 LLVM编译流程(举例)

3.2.1 词法分析

词法分析。词法分析的过程是将源代码按单词/符号,逐个读入,然后生成一个个键值对:
< 单 词 / 符 号 , 属 性 > <单词/符号,属性> </>当然,不全是,也存在一些其他信息。下面以具体代码举例:

源代码

void test(int a, int b){
       int c = a + b - 3;
  }

词法分析得出的结果

void 'void'  [StartOfLine]  Loc=<main.m:18:1>
identifier 'test'    [LeadingSpace] Loc=<main.m:18:6>
l_paren '('     Loc=<main.m:18:10>
int 'int'       Loc=<main.m:18:11>
identifier 'a'   [LeadingSpace] Loc=<main.m:18:15>
comma ','       Loc=<main.m:18:16>
int 'int'    [LeadingSpace] Loc=<main.m:18:18>
identifier 'b'   [LeadingSpace] Loc=<main.m:18:22>
r_paren ')'     Loc=<main.m:18:23>
l_brace '{'     Loc=<main.m:18:24>
int 'int'    [StartOfLine] [LeadingSpace]   Loc=<main.m:19:5>
identifier 'c'   [LeadingSpace] Loc=<main.m:19:9>
equal '='    [LeadingSpace] Loc=<main.m:19:11>
identifier 'a'   [LeadingSpace] Loc=<main.m:19:13>
plus '+'     [LeadingSpace] Loc=<main.m:19:15>
identifier 'b'   [LeadingSpace] Loc=<main.m:19:17>
minus '-'    [LeadingSpace] Loc=<main.m:19:19>
numeric_constant '3'     [LeadingSpace] Loc=<main.m:19:21>
semi ';'        Loc=<main.m:19:22>
r_brace '}'  [StartOfLine]  Loc=<main.m:20:1>
eof ''      Loc=<main.m:20:2>

可以看出,词法分析时,将源代码拆分一个个token,后面数字表示某一行的第几个字符。例如,第一个void,表示第18行第一个字符。

3.2.2 语法分析(生成语法树-AST)

语法分析。语法分析生成语法树(AST,Abstract Syntax Tree)。通过语法树,我们能知道这个代码是做什么的。
($ clang -fmodules -fsyntax-only -Xclang -ast-dump main.m)

语法树

|-FunctionDecl 0x7fa1439f5630 <line:18:1, line:20:1> line:18:6 test 'void (int, int)'
| |-ParmVarDecl 0x7fa1439f54b0 <col:11, col:15> col:15 used a 'int'
| |-ParmVarDecl 0x7fa1439f5528 <col:18, col:22> col:22 used b 'int'
| `-CompoundStmt 0x7fa142167c88 <col:24, line:20:1>
|   `-DeclStmt 0x7fa142167c70 <line:19:5, col:22>
|     `-VarDecl 0x7fa1439f5708 <col:5, col:21> col:9 c 'int' cinit
|       `-BinaryOperator 0x7fa142167c48 <col:13, col:21> 'int' '-'
|         |-BinaryOperator 0x7fa142167c00 <col:13, col:17> 'int' '+'
|         | |-ImplicitCastExpr 0x7fa1439f57b8 <col:13> 'int' <LValueToRValue>
|         | | `-DeclRefExpr 0x7fa1439f5768 <col:13> 'int' lvalue ParmVar 0x7fa1439f54b0 'a' 'int'
|         | `-ImplicitCastExpr 0x7fa1439f57d0 <col:17> 'int' <LValueToRValue>
|         |   `-DeclRefExpr 0x7fa1439f5790 <col:17> 'int' lvalue ParmVar 0x7fa1439f5528 'b' 'int'
|         `-IntegerLiteral 0x7fa142167c28 <col:21> 'int' 3

`-<undeserialized declarations>

语法树(树状图表示)
在这里插入图片描述

3.2.3 语义分析

(暂无)

3.2.4 LLVM IR(中间代码)

(语义分析后,应该就是生成中间代码——LLVM IR了)

LLVM IR有三种等价的表示形式:

      text:便于阅读的文本格式,类似汇编语言。拓展名*.ll;
      ($ clang -S -emit-llvm main.m)

      memory:内存格式;

      bitcode:二进制格式。拓展名*.bc;
      ($ clang -c -emit-llvm main.m)

text格式:

; Function Attrs: noinline nounwind optnone ssp uwtable
define void @test(i32, i32) #2 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i32, align 4
  store i32 %0, i32* %3, align 4
  store i32 %1, i32* %4, align 4
  %6 = load i32, i32* %3, align 4
  %7 = load i32, i32* %4, align 4
  %8 = add nsw i32 %6, %7
  %9 = sub nsw i32 %8, 3
  store i32 %9, i32* %5, align 4
  ret void
}

3.2.5 LLVM IR优化

这一部分主要是使用他人实现pass和自己实现的pass,对LLVM的中间代码(LLVM IR)进行优化。具体参考第4章“LLVM pass”。

3.2.6 目标机器语言

(暂无)

4 LLVM pass

4.1 pass的概念

概念:Pass用于实现对LLVM IR的转化(transformations)和优化(optimizations)。

一些特点
(1)pass只针对LLVM IR进行优化,而无关源码和目标平台。因此,一个pass可以复用于多种高级语言和目标平台的编译场景。

(2)多个pass可以组合使用,来优化某一源码的LLVM IR。LLVM Pass框架会根据不同pass的约束条件,以一种优化的方式调度pass。

(3)所有的LLVM pass都继承于同一根类——Pass类。Pass类的子类图

4.2 一个完整的pass代码样例

#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

namespace {
	
	//Hello是FunctionPass的子类
	struct Hello : public FunctionPass {    
		
		//pass的标识符
		static char ID;               
		Hello() : FunctionPass(ID) {}
	
		//重写继承自FunctionPass中的抽象虚拟方法
		bool runOnFunction(Function &F) override {  
			errs() << "Hello: ";
			errs().write_escaped(F.getName()) << '\n';
			return false;
		}
	};
}

//初始化pass ID,其地址被用来标识该pass
char Hello::ID = 0;

//注册Hello类
//四个参数:命令行参数 = hello ;名称 = Hello World Pass ;
//false:该pass遍历CFG时修改它 ;false:该pass不是一个analysis pass ;
static RegisterPass<Hello> X("hello", "Hello World Pass",
	false /* Only looks at CFG */,
	false /* Analysis Pass */);

/*如果想将该pass注册为现有pipeline的一个步骤,LLVM提供了一些扩展点:
例如 PassManagerBuilder :: EP_EarlyAsPossible可以在任何优化之前应用该pass,
或者PassManagerBuilder :: EP_FullLinkTimeOptimizationLast在链接时间优化后
应用该pass。*/
static llvm::RegisterStandardPasses Y(
	llvm::PassManagerBuilder::EP_EarlyAsPossible,
	[](const llvm::PassManagerBuilder &Builder,
		llvm::legacy::PassManagerBase &PM) { PM.add(new Hello()); });
		

4.3 pass的父类

4.3.1 ImmutablePass

继承该类的pass无需运行、改变状态、更新。它不是转换pass或分析pass,但是可以提供当前编译器的配置信息。

ImmutablePass不常用,但是可以提供有用的信息。继承该类的pass不会失效,不会“运行”,也不会使其他pass无效。

4.3.2 ModulePass

最通用的pass父类。继承该类的pass,会将整个程序作为一个单元。其以不可预测的顺序引用函数体,或添加和删除函数。

由于对ModulePass子类pass的行为一无所知,因此LLVM无法对它们的执行进行优化。

一个module级的pass可以通过接口使用function级的pass(如Dominator Tree),前提是该function级的pass不需要依赖任何Immutable pass、Module pass。

要编写正确的ModulePass子类,可以先继承ModulePass类,然后重载runOnModule()方法:

virtual bool runOnModule(Module &M) = 0;

如果该pass修改了module,则该方法返回true,否则返回false。

4.3.3 CallGraphSCCPass(未细看)

需要在Call Graph上自下而上地遍历程序的pass,可以继承该类。

4.3.4 FunctionPass

不同于ModulePass,FunctionPass子类pass应当具有一个系统可预测的局部行为。所有FunctionPass子类pass在程序中的每个函数上的执行独立于程序中的所有其他函数(就是说要有普适性)。其不需要按照特定顺序执行,且不会修改外部函数。

(4.2中的Hello pass实例就是FunctionPass子类pass)

Functionpass子类pass可以重载三个虚拟方法来完成一些工作(所有这些方法如果修改了程序,则返回true,反之返回false):

(1)doInitialization(Module &)方法
doInitialization方法的设计目的是不依赖于特定函数执行简单的初始化类型的工作(不依赖就是普适性)。该方法能够做Functionpass不允许做的大部分事情。它可以添加和删除函数、获得指向函数的指针等。同时,该方法的调用不会与任何其他遍历执行重叠(因此应该非常快)。

LowerAllocations pass展示了如何使用此方法(一个好例子)。该pass将malloc和free指令转换为依赖于平台的malloc()和free()函数。它使用doInitialization方法获取对其所需的malloc和free函数的引用,并在必要时将原型添加到module中。

(2)runOnFunction方法
runOnFunction方法必须由子类pass实现,用于实现该pass的转换或分析工作。

(3)doFinalization(Module &)方法
doFinalization方法是一种不常用的方法,它只在pass框架完成对正在编译的程序中的每个函数的runOnFunction调用后才被调用。

4.3.5 LoopPass

LoopPass子类pass用于处理程序中的循环,独立于程序中的所有其他循环(普适性)。LoopPass按嵌套顺序处理循环,以便最后处理最外层的循环。

LoopPass子类pass可以使用LPPassManager接口更新循环嵌套。(不懂)

LoopPass子类pass可以重载三个虚拟方法来完成一些工作(所有这些方法如果修改了程序,则返回true,反之返回false):

(1)doInitialization(Loop *, LPPassManager &)方法
doInitialization方法旨在执行简单的初始化类型的工作。这些工作不依赖于特定的函数。doInitialization方法调用不与任何其他pass执行重叠(因此,它应该非常快)。LPPassManager接口应该被用于访问函数或模块级别的分析信息。

(2)runOnLoop方法
该方法必须由子类pass实现,用来进行pass的转换或分析工作。同样,如果修改了该函数,则应返回true。 LPPassManager接口应用于更新循环嵌套。

(3)doFinalization()方法
doFinalization方法是一种不常用的方法,当pass框架完成对正在编译的程序中的每个循环的runOnLoop调用时,将调用该方法。

LoopPass子类pass需要保留所有,同一条流水线中其他LoopPass子类pass同样需要的函数分析。为了简化操作,LoopUtils.h提供了getLoopAnalysisUsage函数。可以在LoopPass子类pass中,重写的getAnalysisUsage函数中调用它,从而获取一致且正确的行为。类似地,INITIALIZE_PASS_DEPENDENCY(LoopPass)将初始化这组函数分析。(不懂)

4.3.6 RegionPass

RegionPass与LoopPass相似,不同的地方在于RegionPass在函数的每个单个的入口和单个的出口区域执行。RegionPass以嵌套顺序处理程序区域,以便最后处理最外面的区域。允许RegionPass子类pass使用RGPassManager接口来更新region tree。(不懂)

RegionPass子类pass可以重载三个虚拟方法来完成一些工作(所有这些方法如果修改了程序,则返回true,反之返回false):

(1)doInitialization(Region *, RGPassManager &)方法
doInitialization方法旨在执行简单的初始化类型的工作。这些工作不依赖于特定的函数。doInitialization方法调用不与任何其他pass执行重叠(因此,它应该非常快)。RPPassManager接口应该被用于访问函数或模块级别的分析信息。

(2)runOnLoop方法
略。

(3)doFinalization()方法
略。

4.3.7 MachineFunctionPass

MachineFunctionPass是LLVM代码生成器的一部分。其中,LLVM代码生成器在程序中每个LLVM函数的机器相关表示上执行。(此处,是生成目标机器码了?)

代码生成器pass被“TargetMachine::addPassesToEmitFile和其他相似度例程”专门的注册和初始化。因此,它们不能使用opt或bugpoint命令运行。

MachineFunctionPass也是一个FunctionPass,其与FunctionPass有同样的限制条件。此外,MachineFunctionPass还有一些额外的限制条件:
(1)不能修改或创建任何LLVM IR指令、基本块、参数、函数、全局变量、全局别名或模块。
(2)不能修改除当前正处理的MachineFunction之外的MachineFunction。
(3)不能跨调用runOnMachineFunction(包括全局数据)来维护状态。

· runOnMachineFunction(MachineFunction &MF)方法
该方法是MachineFunctionPass的主要入口点。所以,应该重写此方法来实现MachineFunctionPass的工作。
该方法会被模块中的每一个MachineFunction调用。因此MachineFunctionPass可以对函数的机器相关的表示执行优化。(优化目标机器码?)

如果想获得正在处理的MachineFunction的LLVM函数,可以使用MachineFunction的getFunction()访问方法。但注意,不能修改来自MachineFunctionPass的LLVM函数或其内容。
(这里的MachineFunction、LLVM函数是啥不清楚。)

4.4 pass的注册

4.4.1 注册方法

上述Hello pass案例中有。

4.4.2 print方法

如果想让pass更容易被转储,你应该实现虚拟print方法:(转储?)

virtual void print(llvm::raw_ostream &O, const Module *M) const;

为了打印出可读版本的分析结果,print方法必须通过“分析(analyses)”实现,以便打印分析结果的可读版本。 这有助于于调试分析以及其他人弄清楚分析是怎样工作的。 使用opt的-analyze参数来调用此方法。(这里的analyses是啥?analyses pass吗?)

llvm :: raw_ostream参数指定了写入结果的流,而Module参数给出了指向该程序已被分析的top模块的指针。但是请注意,此指针在某些情况下可能为空(例如,从调试器调用Pass :: dump()).因此,该方法应仅被用于增强调试输出,而不应该被依赖。

4.5 如何指定pass之间的交互

Pass Manager的主要职责之一是确保pass之间能够正确交互。因为,Pass Manager会试图优化pass的执行,所以它必须知道pass之间如何交互以及各个pass之间存在哪些依赖关系。

为了获得这些信息,每个pass可以声明在当前pass之前需要执行的pass,以及当前pass导致无效的pass。这个功能用于确保在运行某pass之前,计算出该pass需要的分析结果。

运行任意的转换pass会让之前计算出来的分析结果无效。另外,如果某个pass没有实现getAnalysisUsage方法,则默认为它没有任何先决的pass,并且会使所有其他pass无效。

4.5.1 getAnalysisUsage方法

getAnalysisUsage方法可以为该转换pass指定需要的pass集和无效的pass集。因此,实现getAnalysisUsage方法,需要在AnalysisUsage对象中填充一些信息:哪些pass是需要的,且不是无效的。这可以通过调用下面的方法实现(AnalysisUsage对象的方法):

(1)AnalysisUsage::addRequired<>方法、AnalysisUsage::addRequiredTransitive<>方法
如果该pass执行前需要先执行其他pass(如,一个分析pass),可以使用标题里两个方法中的一个来设置。

LLVM有许多不同类型的pass(从DominatorSet到BreakCriticalEdges)。如,BreakCriticalEdges类型可以确保该pass在运行时,CFG(Control Flow Graph,控制流图)中没有关键边缘。

分析pass与分析pass之间可以链接起来,共同完成任务。如,AliasAnalysis<AliasAnalysis>的实现需要链接到其他的别名分析pass。在分析链的情况下,应该使用addRequiredTransitive方法而不是addRequired方法。因为,前者会通知Pass Manager,提供分析结果的pass应该与需要分析结果的pass存在的时间一样长。

(2)AnalysisUsage::addPreserved<>方法
Pass Manager的工作之一是优化分析运行的方式和时间。除非必要,否则它一直避免重复计算。所以,pass是可以声明保留现有分析结果的(即不使其无效,防止被Pass Manager给优化掉)。如,一个简单的常数折叠pass不会修改CFG,因此它不会影响dominator分析的结果。但是,在默认情况下,会假定所有的pass会使其他pass无效。
AnalysisUsage类提供了几种方法,这些方法在和addPreserved相关的情况下是有用的。如,可以调用setPreservesAll方法来表明该pass不会修改LLVM程序。
同时,一些会更改程序中的指令,但不会修改CFG或terminator指令的转换pass可以调用setPreservesCFG方法。
addPreserved方法对于BreakCriticalEdges等类型的转换pass特别有用。因为,这些转换pass知道如何更新一小部分循环以及dominator相关的分析。因此,可以使用addPreserved方法保留它们,尽管这样改变了CFG。(不太懂)

4.5.2 getAnalysisUsage的实现样例

// This example modifies the program, but does not modify the CFG
void LICM::getAnalysisUsage(AnalysisUsage &AU) const {
  AU.setPreservesCFG();
  AU.addRequired<LoopInfoWrapperPass>();
}

4.5.3 getAnalysis<>方法 、getAnalysisIfAvailable<>方法

(1)getAnalysis<>方法
pass::getAnalysis<>方法会被类自动继承,它提供对申明了getAnalysisUsage方法的pass的访问接口。它使用一个单独的模板参数来指定你想要的pass类,并返回该pass的引用。如:

bool LICM::runOnFunction(Function &F) {
  LoopInfo &LI = getAnalysis<LoopInfoWrapperPass>().getLoopInfo();
  //...
}

该方法的调用会返回所需pass的引用。如果你试图获取一个分析pass,但该pass没有进行getAnalysisUsage实现所要求的声明,则可能会出现runtime assertion failure错误。该方法可以在pass的run*方法中调用,也可以由run*方法调用的任何其他本地方法调用。一个模块级pass可以使用该方法来获取函数级的分析信息。具体如下:

bool ModuleLevelPass::runOnModule(Module &M) {
  //...
  DominatorTree &DT = getAnalysis<DominatorTree>(Func);
  //...
}

在上面的示例中,Pass Manager在返回所需pass的引用之前,调用了DominatorTree的runOnFunction。

(2)getAnalysisIfAvailable<>方法
如果一个pass能够更新分析(如,上面说的BreakCriticalEdges),那么就可以使用getAnalysisIfAvailable方法。如果分析是active的,该方法将返回一个指向该分析pass的指针。如:

if (DominatorSet *DS = getAnalysisIfAvailable<DominatorSet>()) {
  // A DominatorSet is active.  This code will update it.
}

4.6 分析组(Analysis Groups)

4.6.1 分析组的概念

现在,我们已经了解了pass的定义,使用方式以及从pass中获取其他pass的方法。但是,目前为止,我们看到的pass间的关系都非常简单:一个pass运行前,可能需要先运行其他特定的pass。而对于一些复杂的应用,可能需要更加灵活的pass关系。尤其是一些分析pass定义了:分析结果只有一个简单的接口,但有多种不同的计算方法。就拿别名分析来说。最简单的别名分析就是给任意的别名查询返回“may alias”。而最复杂的分析是流程敏感、上下文敏感的过程间分析,该过程间分析可能需要大量的执行时间(显然,这两个极端之间是需要根据需求调节的)。为此,LLVM提出了分析组的概念。

分析组是一个简单的接口,可由多个不同的pass来实现。分析组的名字可以像pass一样来指定,但是它不从pass类派生。分析组可以有一个或多个pass,其中有一个是“默认”pass。和pass一样,分析组也可以被pass调用,使用相同的方法:AnalysisUsage::addRequired()和pass::getAnalysis()。调用这两个方法时,Pass Manager会扫描所有可用的pass来查看有没有可用的分析组。如果没有,则会创建一个“默认”的分析组pass供其他pass使用。

对于一般的pass来说,“ pass注册”是可选的。但是,分析组必须被注册,并且需要使用INITIALIZE_AG_PASS模板来加入“实现池”(implementation pool)。另外,分析组的默认接口pass必须使用RegisterAnalysisGroup注册。

AliasAnalysis分析组是分析组的一个具体示例。该分析组的默认接口pass(basicaa pass)仅进行了一些简单的检查操作(如,两个不同的全局变量不能互为别名等)。使用AliasAnalysis分析组的pass(如,gvn pass)不考虑其实现了什么功能,仅需使用指定的接口。

而从用户的角度来看,使用分析组的命令和使用正常pass的命令相同。如,使用命令“opt -gvn…”将会实例化basicaa pass并将其添加到pass序列中。使用命令“opt -somefancyaa -gvn …”会让gvn pass改为使用somefancyaa别名分析(实际上并不存在,这只是一个假设的例子)。

4.6.1 使用RegisterAnalysisGroup注册分析组

RegisterAnalysisGroup模板用于分析组的注册,而INITIALIZE_AG_PASS用于在分析组里添加pass。

首先,注册一个分析组,并为其命名(与pass的注册不同,Analysis Group注册不需要指定命令行参数,因为它是“抽象的”):

static RegisterAnalysisGroup<AliasAnalysis> A("Alias Analysis");

分析组被注册后,passes可以使用以下代码声明它们“加入”分析组:

namespace {
  // Declare that we implement the AliasAnalysis interface
  INITIALIZE_AG_PASS(FancyAA, AliasAnalysis , "somefancyaa",
      "A more complex alias analysis implementation",
      false,  // Is CFG Only?
      true,   // Is Analysis?
      false); // Is default Analysis Group implementation?
}

上面仅仅显示了FancyAA pass,它使用INITIALIZE_AG_PASS宏来注册并“加入”AliasAnalysis分析组。分析组的每个pass都应该使用这个宏进行“加入”。

下面代码展示了如何指定默认pass(使用INITIALIZE_AG_PASS模板的最后一个参数)。对于要使用的分析组,必须有一个始终可用的缺省pass(ImmutablePass仅可以派生默认pass)。下面代码中声明了BasicAliasAnalysis pass是默认pass:

namespace {
  // Declare that we implement the AliasAnalysis interface
  INITIALIZE_AG_PASS(BasicAA, AliasAnalysis, "basicaa",
      "Basic Alias Analysis (default AA impl)",
      false, // Is CFG Only?
      true,  // Is Analysis?
      true); // Is default Analysis Group implementation?
}

4.7 分析统计(Pass Statistics)

统计类用于统计所有pass完成的操作。在命令行中加上-stats选项,可以打印统计信息(详细信息参阅程序员手册中的Statistics部分)。参考下面的命令:

$ opt -stats -mypassname < program.bc > /dev/null
... statistics output ...

统计信息的输出:

  7646 bitcodewriter   - Number of normal instructions
   725 bitcodewriter   - Number of oversized instructions
129996 bitcodewriter   - Number of bitcode bytes written
  2817 raise           - Number of insts DCEd or constprop'd
  3213 raise           - Number of cast-of-self removed
  5046 raise           - Number of expression trees converted
    75 raise           - Number of other getelementptr's formed
   138 raise           - Number of load/store peepholes
    42 deadtypeelim    - Number of unused typenames removed from symtab
   392 funcresolve     - Number of varargs functions resolved
    27 globaldce       - Number of global variables removed
     2 adce            - Number of basic blocks removed
   134 cee             - Number of branches revectored
    49 cee             - Number of setcc instruction eliminated
   532 gcse            - Number of loads removed
  2919 gcse            - Number of instructions removed
    86 indvars         - Number of canonical indvars added
    87 indvars         - Number of aux indvars removed
    25 instcombine     - Number of dead inst eliminate
   434 instcombine     - Number of insts combined
   248 licm            - Number of load insts hoisted
  1298 licm            - Number of insts hoisted to a loop pre-header
     3 licm            - Number of insts hoisted to multiple loop preds (bad, no loop pre-header)
    75 mem2reg         - Number of alloca's promoted
  1444 cfgsimplify     - Number of blocks simplified

4.8 Pass Manager

4.8.1 Pass Manager

PassManager类基于一个pass列表来调度pass,从而保证高效的运行。所有LLVM工具都通过Pass Manager来执行pass。Pass Manager的实现在lib/Transforms/IPO/中,其中PassManagerBuilder.cpp是最主要的文件。

Pass Manager主要做两件事来减少pass的执行时间:
(1)共享分析结果。Pass Manager会尽可能的避免重复的计算。所以,Pass Manager需要知道pass之间的交互(如,哪些分析可用,哪些分析无效以及pass间运行的先后顺序)。特别是,Pass Manager需要知道分析结果的生命周期,从而能够在不再需要这些结果时释放内存。
(2)在程序上流水线式的执行pass。为了保证更好的缓存和内存的使用,Pass Manager会将pass的执行流水线化。如,给定一系列连续的FunctionPass,LLVM会在程序的第一个函数上执行完所有FunctionPass,然后在第二个函数上执行所有FunctionPass,依此类推,直到遍历完整个程序。

Pass Manager的有效性会受到它所调度的pass行为的信息量的影响。如,“preserved”集遇到未实现getAnalysisUsage方法的pass时,会刻意的保守。在实现某pass的时候没有实现getAnalysisUsage方法,会导致在执行该pass时不允许任何分析结果存在(就是说执行此pass时会关闭其他分析,就像下面的Hello World pass的例子一样)。

4.8.2 Pass Manager提供的debug方法

PassManager类提供了“–debug-pass”命令行选项。这个选项对调试pass十分有用,如,查看pass的工作方式、诊断分析结果在何时关闭何时保留等。(如果要获取有关–debug-pass选项的所有变种选项的信息,可以使用命令“ opt -help-hidden”获取)。如,使用“–debug-pass = Structure”选项,可以看到Hello World pass是如何与其他pass交互的。

接下来,我们使用一下这个选项:
首先,先用gvn pass和licm pass单独测试一下(即下面输出中的Global Value Numbering和Loop Invariant Code Motion)。下面结构化的输出中,可以看到gvn pass使用了dominator tree construction来帮助自己完成工作。licm pass既使用了natural loop information,同时也使用了dominator tree construction。而在licm pass后面,执行了module verifier(opt工具自动加的)。module verifier里也使用了dominator tree construction,主要用来检查得到的LLVM代码是否被很好的格式化了。可以看到,这里dominator tree construction只计算了一次,但被三个pass共享了:
(这里没看到dominator tree construction的关闭啊,是全局都需要用到吗?)

$ opt -load lib/LLVMHello.so -gvn -licm --debug-pass=Structure < hello.bc > /dev/null
ModulePass Manager
  FunctionPass Manager
    Dominator Tree Construction
    Basic Alias Analysis (stateless AA impl)
    Function Alias Analysis Results
    Memory Dependence Analysis
    Global Value Numbering
    Natural Loop Information
    Canonicalize natural loops
    Loop-Closed SSA Form Pass
    Basic Alias Analysis (stateless AA impl)
    Function Alias Analysis Results
    Scalar Evolution Analysis
    Loop Pass Manager
      Loop Invariant Code Motion
    Module Verifier
  Bitcode Writer

接下来,我们看下将Hello World pass放在两个pass中间运行得到的输出会有什么不同:

$ opt -load lib/LLVMHello.so -gvn -hello -licm --debug-pass=Structure < hello.bc > /dev/null
ModulePass Manager
  FunctionPass Manager
    Dominator Tree Construction
    Basic Alias Analysis (stateless AA impl)
    Function Alias Analysis Results
    Memory Dependence Analysis
    Global Value Numbering
    Hello World Pass
    Dominator Tree Construction
    Natural Loop Information
    Canonicalize natural loops
    Loop-Closed SSA Form Pass
    Basic Alias Analysis (stateless AA impl)
    Function Alias Analysis Results
    Scalar Evolution Analysis
    Loop Pass Manager
      Loop Invariant Code Motion
    Module Verifier
  Bitcode Writer
Hello: __main
Hello: puts
Hello: main

上面输出可以看出,Hello World pass终止了dominator tree construction,即使Hello World pass并没有修改程序。这样的话,在后面需要用到dominator tree construction的时候,需要再计算一次,这明显是浪费。

为了解决该问题,我们在Hello World pass中添加getAnalysisUsage方法的实现:

// We don't modify the program, so we preserve all analyses
void getAnalysisUsage(AnalysisUsage &AU) const override {
  AU.setPreservesAll();
}

再次运行pass后,看到dominator tree construction没有再被终止,也就不会再计算两次了:

$ opt -load lib/LLVMHello.so -gvn -hello -licm --debug-pass=Structure < hello.bc > /dev/null
Pass Arguments:  -gvn -hello -licm
ModulePass Manager
  FunctionPass Manager
    Dominator Tree Construction
    Basic Alias Analysis (stateless AA impl)
    Function Alias Analysis Results
    Memory Dependence Analysis
    Global Value Numbering
    Hello World Pass
    Natural Loop Information
    Canonicalize natural loops
    Loop-Closed SSA Form Pass
    Basic Alias Analysis (stateless AA impl)
    Function Alias Analysis Results
    Scalar Evolution Analysis
    Loop Pass Manager
      Loop Invariant Code Motion
    Module Verifier
  Bitcode Writer
Hello: __main
Hello: puts
Hello: main

4.8.3 releaseMemory方法

Pass Manager会自行抉择何时计算分析结果以及将其保留多长时间。实际上,pass对象本身的生命周期是整个编译过程,所以当它们不再有用时,应该使用某种方法来释放分析结果。releaseMemory虚拟方法就是用来执行此操作的。

如果一个分析pass或其他pass,需要保留大量信息供其他pass使用。则应实现releaseMemory方法来在合适的时候释放分配给该pass的内存。这个方法的调用,在该pass的run *方法之后,该pass的下一次run *调用之前。(应该也是Pass Manager里的实现)

4.9 如何注册“动态加载”的pass

当使用LLVM构建工具时,大小很重要。这既是为了分发(distribution),也是为了在目标系统上运行时,调节驻留代码的大小。(不懂)
因此,应该有选择地使用一些pass,并保持pass调用的灵活性。另外,还需要向用户提供一些反馈。上述这些就是pass注册的作用。

Pass注册机制:MachinePassRegistry类和MachinePassRegistryNode的子类。

MachinePassRegistry的实例用于保存MachinePassRegistryNode对象的列表,并从命令行接口获取添加和删除信息。
一个MachinePassRegistryNode子类的实例即存储了一个pass的信息,包括:命令行名,命令帮助信息,创建pass实例的函数的地址。一个实例的一个全局静态构造函数使用相应的MachinePassRegistry(一个静态析构函数unregisters)注册。(不懂)
因此,工具中静态链接的pass将在启动时被注册。而动态加载的pass将在加载时注册,在卸载时注销。

4.9.1 如何使用现有的注册

有一些预定义的注册,被用来跟踪指令调度(如,RegisterScheduler)和register分配(RegisterRegAlloc)机器pass。接下来,将描述如何注册一个register allocator machine pass。
首先,需要实现一个register allocator machine pass。在register allocator的.cpp文件中添加include:

#include "llvm/CodeGen/RegAllocRegistry.h"

定义一个creator函数。注意,该函数的署名应与RegisterRegAlloc::FunctionPassCtor的类型匹配:

FunctionPass *createMyRegisterAllocator() {
  return new MyRegisterAllocator();
}

再添加“installing”申明:

static RegisterRegAlloc myRegAlloc("myregalloc",
                                   "my register allocator help string",
                                   createMyRegisterAllocator);

可以看到,在-help查询里-regalloc选项生成了myregalloc参数:

$ llc -help
  ...
  -regalloc                    - Register allocator to use (default=linearscan)
    =linearscan                -   linear scan register allocator
    =local                     -   local register allocator
    =simple                    -   simple register allocator
    =myregalloc                -   my register allocator help string
  ...

现在,用户可以自由的使用-regalloc = myregalloc作为命令行的选项了。除RegisterScheduler类之外,其他类与Registering指令调度器相似。注意,RegisterScheduler :: FunctionPassCtor与RegisterRegAlloc :: FunctionPassCtor有很大不同。

如果要强制将register allocator加载/链接到llc/lli工具中,需要将creator函数的全局声明添加到Passes.h里,并在llvm/Codegen/LinkAllCodegenComponents.h中添加一个“伪”调用行。

4.9.2 如何创建新的注册

最简单的方法就是复制现有的注册。官方建议使用llvm/CodeGen/RegAllocRegistry.h。

其中,主要需要修改的是类名和FunctionPassCtor类型。

然后,需要声明注册。例如,如果你的pass注册为RegisterMyPasses,则定义:

MachinePassRegistry RegisterMyPasses::Registry;

最后,为pass申明命令行选项。例如:

cl::opt<RegisterMyPasses::FunctionPassCtor, false,
        RegisterPassParser<RegisterMyPasses> >
MyPassOpt("mypass",
          cl::init(&createDefaultMyPass),
          cl::desc("my pass option help"));

上面的实现,命令行选项是“mypass”,createDefaultMyPass是默认的创建者。

4.10 如何将GDB和动态加载pass一起使用

(GDB是Unix下的一种调试工具)

将GDB与动态加载pass一起使用比较难,因为:首先,你无法在一个未被加载的共享对象中设置断点。其次,共享对象中的内联函数存在问题。

下面是使用GDB调试pass的几个建议:
(为了方便讨论,假设你正在调试一个用opt调用的transformation)

4.10.1 在pass中设置断点

首先,在opt进程中启动GDB(注意,opt有很多调试信息,所以加载需要时间,耐心等)。(看命令顺序,感觉像是说反了)

$ gdb opt
GNU gdb 5.0
Copyright 2000 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "sparc-sun-solaris2.6"...
(gdb)

由于不能在pass中设置断点(共享对象在运行时才会被加载),我们需要执行进程,并在启动我们的pass前,加载共享对象之后,中止它。最简单的方法是在PassManager::run中设置断点,然后使用你想要的参数运行进程:

$ (gdb) break llvm::PassManager::run
Breakpoint 1 at 0x2413bc: file Pass.cpp, line 70.
(gdb) run test.bc -load $(LLVMTOP)/llvm/Debug+Asserts/lib/[libname].so -[passoption]
Starting program: opt test.bc -load $(LLVMTOP)/llvm/Debug+Asserts/lib/[libname].so -[passoption]
Breakpoint 1, PassManager::run (this=0xffbef174, M=@0x70b298) at Pass.cpp:70
70      bool PassManager::run(Module &M) { return PM->run(M); }
(gdb)

一旦opt被PassManager::run方法中止,你就可以自由的在你的pass中设置断点了。这样你就可以进行执行跟踪或者其他标准的调试工作了。

4.10.2 存在问题

有了基础后,就会发现GDB存在几个问题,有的有解决方案,有的没有。

(1)内联函数存在伪堆栈信息。一般情况下,GDB会在获取堆栈跟踪和遍历内联函数方面做得很好。但是,如果动态加载了pass,上述功能就会丧失。已知的唯一解决方案是:取消内联函数(将其从类的主体移至一个.cpp文件)。

(2)重新启动程序会破坏断点。遵循了上述操作后,你已经成功在pass中植入了一些断点。然而之后,如果你重启程序(即再次键入“运行”),会收到无法设置断点的错误。已知“解决”此问题的唯一方法是:删除pass中已设置的断点,运行程序,并在被PassManager :: run方法停止执行后重新设置断点。

5 opt工具

(参考资料:opt - LLVM optimizer

opt:opt命令是模块化的LLVM优化器。它以LLVM字节码作为输入,对其进行指定的优化,然后输出优化的LLVM字节码。

通过opt进行的优化取决于链接到其中的库,以及已使用-load选项加载的任何其他库。使用-help选项来确定可以使用的优化。(就是可用的pass)

如果在命令行上未指定文件名,则opt从标准输入读取其输入。如果未使用-o选项指定输出文件名,则opt将其输出写入标准输出。

使用RegisterPass注册过的pass都可以使用opt的-load选项访问。

(opt的相关指令选项参考上述参考资料)

6 LLVM源码结构(未看)

参考资料:
(1)LLVM每日谈之六 LLVM的源码结构:https://yq.aliyun.com/articles/233376?spm=a2c4e.11153940.0.0.116c2424KcQ3Ce
(2)Getting Started with the LLVM System:http://llvm.org/docs/GettingStarted.html#getting-started
(3)Creating an LLVM Project:http://llvm.org/docs/Projects.html

7 Clang的自定义属性(未看)

参考资料:
(1)自定义clang的Attribute:https://www.jianshu.com/p/26343edd25f3
(2)Clang 之旅–[翻译]添加自定义的 attribute:https://www.jianshu.com/p/d277c42f4907
(3)How to add an attribute:https://clang.llvm.org/docs/InternalsManual.html#how-to-add-an-attribute
(4)使用Clang作为编译器 —— Clang 中的属性:https://blog.youkuaiyun.com/qq_23599965/article/details/90897476)

8 LLVM后端开发(未看)

参考资料:
(1)LLVM后端开发:https://blog.youkuaiyun.com/jinweifu/article/details/54132939

参考资料

(1)《编译系统透视 图解编译原理》
(2)深入浅出让你理解什么是LLVM
(3)基于LLVM 中间表示(IR)分析实例
(4)Writing an LLVM Pass(LLVM官方文档)
(5)LLVM-Pass调试相关
(6)opt - LLVM optimizer

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值