代码优化将LLVM IR转变为消耗较少资源的代码,如内存。在这篇文章中,我们将学习如何将不同的编译器优化应用于产生的LLVM IR。
目录
- 简介
- 共同表达式的消除
- 常数折叠
- LLVM优化通道
- 总结
- 参考资料
先决条件
介绍。
在先决条件的文章中,我们看到了如何在编译器设计的代码生成阶段生成LLVM IR(Intermediate representation)。这也是编译的中间阶段,现在代码被传递到编译器后端,在那里它可以被转化为与机器相关的目标代码。
在这一点上,我们应该有一个REPL,据此我们在Kaleidoscope编程语言中输入代码,并将其翻译成LLVM IR(中间表示法)。
比如说。
如果我们输入4+4,LLVM IR看起来如下:
Read top-level expression:define double @0() {
entry:
ret double 8.000000e+00
}
解析器将编程顶层包裹在一个函数中,我们创建并命名了一个块 "entry"。
最后,LLVM将两个常数加在一起,得到8
,这也是一个常数。
在这篇文章中,我们将实现各种优化以产生紧凑的LLVM IR。优化可以是局部的,也可以是全局的,前者指的是对单个基本块内的代码所做的修改,后者涉及对整个函数体/方法/程序的优化。
共用表达式的消除。
在我们将源代码翻译成中间代码后,有几处出现了类似的计算。例如,我们有以下赋值ar[i] = := ar[i] + 1,它被翻译成以下中间代码:
t1 := 4*i
t2 := a+t1
t3 := M[t2]
t4 := t3+1
t5 := 4*i
t6 := a+t5
M[t6] := t4
以上,我们进行了4的乘法和两次加法(t1, t5)和(t2, t6)。我们在普通子表达式消除中的目标是去除这种冗余。
另一方面,常量折叠在编译时对常量表达式进行评估,并用它们的值来替换它们。例如,我们有以下常量表达式;2 * 3.14,其值为6.28。
常量折叠。
我们将使用IRBuilder,它在编译IR时为我们提供常见的优化,如常数折叠。它也没有语法上的开销,并大大减少了LLVM IR的生成量。
下面是Kaleidoscope编程语言的常数折叠的实际情况;
如下;
def test(x) 1+2+x;
常数1和2被合并产生3 ,如下面的LLVM IR所示:
Read function definition:
define double @test(double %x) {
entry:
%addtmp = fadd double 3.000000e+00, %x
ret double %addtmp
}
在LLVM中,所有对构建LLVM IR的调用都要经过LLVM IR构建器。构建器会检查是否有机会进行常数折叠,如果有,就会进行常数折叠,并返回常数,而不是创建一条指令。
LLVM IR生成器的局限性在于,它在代码被构建的过程中进行分析。例如,对于以下语句。
def test(x) (1+2+x)*(x+(1+2)); ,我们有以下LLVM IR:
ready> Read function definition:
define double @test(double %x) {
entry:
%addtmp = fadd double 3.000000e+00, %x
%addtmp1 = fadd double %x, 3.000000e+00
%multmp = fmul double %addtmp, %addtmp1
ret double %multmp
}
上面,LHS(左侧)和RHS(右侧)的值是一样的。我们希望它能生成*"tmp = x+3; result = tmptmp*; "*,而不是计算两次*"x+3"*。
然而,局部分析不能检测和纠正这一点,因为它需要两次转换,表达式的重新关联,以及消除共同的子表达式来消除冗余。幸运的是,LLVM以通行证的形式提供优化,我们在下一节讨论。
LLVM优化通行证
LLVM提供的优化通道有很多帮助,但有不同的权衡。LLVM允许编译器实现者对使用的优化、顺序和情况做出决定。
例如,LLVM支持整个模块传递和每个函数传递,前者看整个代码体,而后者一次只看一个函数。
我们将在用户键入一个函数时实现每个函数的优化。为此,我们需要设置FunctionPassManager,它将保存和组织我们想要运行的LLVM优化。
下面,我们创建一个函数来创建和初始化模块和传递管理器:
void InitializeModuleAndPassManager(void)
{
TheModule = make_unique<Module>("JIT AND OPTIMIZATION", *TheContext); // create new module
TheFPM = make_unique<legacy::FunctionPassManager>(TheModule.get()); // attach a pass manager
TheFPM->add(createInstructionCombiningPass()); // peephole and bit-twiddling optimizations
TheFPM->add(createReassociatePass()); // reassociation expressions
TheFPM->add(createGVNPass()); // common subexpression elimination
TheFPM->add(createCFGSimplificationPass()); // removing unreachable blocks -> simple control flow graph
TheFPM->doInitialization();
}
上面我们初始化了一个全局模块TheModuke和FPM,即函数传递管理器。一旦通行证管理器设置完毕,我们就添加LLVM通行证。
我们添加了四个优化,它们是非常有用的标准清理优化。
在我们新创建的函数被构造出来后,但在它被返回给客户端之前,我们运行传递管理器。
我们在代码生成函数中添加以下一行:
TheFPM->run(*TheFunction); // optimize
让我们来测试一下。
通过实现的优化,我们期望以下表达式;
def test(x) (1+2+x)*(x+(1+2));
产生以下优化的LLVM IR:
ready> Read function definition:define double @test(double %x) {
entry:
%addtmp = fadd double %x, 3.000000e+00
%multmp = fmul double %addtmp, %addtmp
ret double %multmp
}
与之前的例子相比,现在我们只有一个添加。
总结
JIT(Just In Time)编译提供了最好的编译或解释。它通过允许程序以可执行的可移植格式存储,并在其在目标机器上执行前不久对其进行编译。
我们已经学会了如何添加一个支持Kaleidoscope编程语言的优化器。