LLVM编译器的优化

1,024 阅读5分钟

代码优化将LLVM IR转变为消耗较少资源的代码,如内存。在这篇文章中,我们将学习如何将不同的编译器优化应用于产生的LLVM IR。

目录

  1. 简介
  2. 共同表达式的消除
  3. 常数折叠
  4. LLVM优化通道
  5. 总结
  6. 参考资料

先决条件

  1. 向LLVM IR生成代码

介绍。

在先决条件的文章中,我们看到了如何在编译器设计的代码生成阶段生成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;
常数12被合并产生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();
}

上面我们初始化了一个全局模块TheModukeFPM,即函数传递管理器。一旦通行证管理器设置完毕,我们就添加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编程语言的优化器。

参考资料

  1. 如何编写通证
  2. LLVM通证
  3. 常量折叠
  4. 独立于机器的优化
  5. 普通子表达式的消除