写给入门者的LLVM介绍 - 知乎 (zhihu.com)
代码优化与LLVM IR pass | Kiprey’s Blog
A Tour to LLVM IR(上) - 知乎 (zhihu.com)
第5章 LLVM中间表示 — Getting Started with LLVM Core Libraries 文档 (getting-started-with-llvm-core-libraries-zh-cn.readthedocs.io)
LLVM是一个编译器(确切的说是一套框架+基于框架的一些编译器实现)
LLVM IR(Intermediate Representation)是LLVM的一种中间表示,也可以视为中间代码(便于代码优化)
LLVM之所以优秀,在于以下几点:
有时候一项工作看起来并不完全是个完整的编译行为,但只要涉及到源码到源码的转换,了解LLVM通常会有所帮助。
以下是一些使用 LLVM 完成并非所有编译操作的研究项目的示例:
这张图片显示了 LLVM 架构的主要部件:
LLVM IR 具有三种表示形式,这三种中间格式是完全等价的:
在内存中的编译中间语言(我们无法通过文件的形式得到)
在硬盘上存储的二进制中间语言(bitcode形式,格式为 .bc)
人类可读的代码语言(LLVM汇编文件形式,格式为 .ll)
汇编形式的IR是可读的。所以这里用一个简单的例子展示一下汇编形式的IR。首先编写一个简单的c语言函数如下:
// add.cpp
int add(int a, int b) {
return a + b;
}
使用如下命令可以产生汇编形式的IR:
clang add.cpp -emit-llvm -S -c -o add.ll
具体的汇编IR如下:
; 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: noinline nounwind optnone ssp uwtable
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
}
source_filename
是表明这个module是从什么文件编译得到的(如果你打开main.ll
会发现这里的值是main.cpp
),如果该modules是通过链接得到的,这里的值就会是llvm-link
。@main
中的 %1
是一个 i32*
类型的地址,而 @foo
中的 %1
是一个 i32
类型的值)@main
函数中的 %1
,%2
,%3
)@
为前缀,局部变量以 %
为前缀alloca
分配内存并返回地址,load
从内存读出值,store
向内存存值,add
用于加法等)Module包含Functions,其中包含BasicBlocks,其中包含Instructions。除了 Module 之外的所有东西都来自Value。
![[Snipaste_2023-11-28_16-19-11.png|125]]
Module类,Module可以理解为一个完整的编译单元。一般来说,这个编译单元就是一个源码文件,比如一个后缀为cpp的源文件。每个module之间相互独立,module主要包含了声明或者定义的函数、全局变量等。
Function类,就是对应一个函数单元。主要包含了大量 BasicBlock 、参数和返回值类型、可变参数列表、函数的attribute和其他函数的基本信息Function由无数个 BasicBlock组成,使用列表存放,有且仅有一个 EntryBlock ,是列表中的第一个 BasicBlock,代码真正执行的时候,就从 EntryBlock开始执行。
BasicBlock类,这个类表示一个基本代码块,“基本代码块”就是一段没有控制流逻辑的基本流程,相当于程序流程图中的基本过程。控制流只能从基本块的第一个指令进入该块。也就是说,没有跳转到基本块中间的或末尾指令的转移指令。除了基本块的最后一个指令,控制流在离开基本块之前不会跳转或停机。
Instruction类,指令类就是LLVM中定义的基本操作,比如加减乘除这种算数指令、函数调用指令、跳转指令、返回指令等等
下面,我们通过一个例子来介绍程序的控制流是如何通过基本块与终结指令描述的:
if.c
导出为 LLVM IR
这个程序的控制流如图所示(点一下PPT)
br i1 %9, label %10, label %11 ; A
br label %12 ; B
br label %12 ; C
br
指令一共在代码中出现了三次
在这里,我们先介绍一下br
指令的用法, br
指令的语法为 br + 标志位 + truelabel + falselabel
,或者br + label
形如上面代码中 A
用法的转移指令叫做条件转移,如果标志位为1
,程序会跳往truelabel
标记的basicblock
。如果标志位为0
,程序会跳往falseblock
标记的basicblock
。比如,在代码br i1 %9, label %10, label %11
中,如果%9
的值为1
,就会跳转往基本块%10
,如果为0
,就会跳转往基本块%11
。
形如上面代码中B,C
的用法的转移指令叫做无条件转移,他会在程序运行到此处时无条件跳转到目标基本块。在上面代码中B,C
两处的代码都会无条件跳转到基本块%12
。
如上图所示,%9
是icmp eq
指令(用来判断两个值是否相等,我们会在_推荐使用的指令_一节详细介绍)的结果,如果%7
等于%8
,那么%9
的值就会为1
,否则为0
。这条指令对应了源代码中的if(a == b)
,c=5
对应了基本块%10
,c=10
对应了基本块%11
,这两个基本块运行结束时都需要跳转到目标基本块%12
执行后面的语句putint(c)
以及return 0
。
删除公共子表达式
如果表达式x op y
先前已被计算过,并且从先前的计算到现在,x op y
中的变量值没有改变,则x op y
的这次出现就称为公共子表达式(common subexpression)
删除无用代码
无用代码(Dead-code):其计算结果永远不会被使用的语句
常量合并
如果在编译时刻推导出一个表达式的值是常量,就可以使用该常量来替代这个表达式。该技术被称为 常量合并
代码移动
这个转换的结果是那些 不管循环多少次都得到相同结果的表达式(即循环不变计算,loop-invariant computation),在进入循环之前就对它们进行求值。
强度削弱用较快的操作代替较慢的操作,如用 加 代替 乘 。(例:2*x ⇒ x+x)
删除归纳变量
对于一个变量x ,如果存在一个正的或负的常数c使得每次x被赋值时它的值总增加c ,那么x就称为归纳变量(Induction Variable)。在沿着循环运行时,如果有一组归纳变量的值的变化保持步调一致,常常可以将这组变量删除为只剩一个
为了优化给定的代码段,我们可以应用几种常见的编译器优化技术。这些包括但不限于常量传播、代数化简、强度削弱和消除冗余代码。下面是按阶段进行的优化步骤,以及每个阶段的代码变化情况:
T1 = j - 2;
T2 = 4 * T1;
temp = A[T2];
T3 = j + 2;
T4 = T3 - 2;
T5 = 8 * T4;
T6 = A[T5];
T1
和 T4
实际上是相同的操作,都是 j - 2
。T2
是 4 * (j - 2)
,可以直接计算。T3
是 j + 2
,T4
是 T3 - 2
,所以 T4
实际上就是 j
。T5
是 8 * j
(因为 T4
现在是 j
)。优化后的代码:
T1 = j - 2; // 或者可以直接在T2中使用j - 2,从而完全省略这一行
T2 = 4 * (j - 2); // 代数化简
temp = A[T2];
// T3 = j + 2; // 不再需要,因为T4和T1相等
// T4 = j; // 不再需要,因为T4和T1相等
T5 = 8 * j; // 代数化简
T6 = A[T5];
T1
和 T4
相等,我们可以消除 T4
。T2
和 T5
的赋值中使用 j - 2
和 j
,进一步消除 T1
和 T4
。优化后的代码:
T2 = 4 * (j - 2);
temp = A[T2];
T5 = 8 * j;
T6 = A[T5];
T2 = 4 * (j - 2);
temp = A[T2];
T5 = 8 * j;
T6 = A[T5];
这个优化过程消除了冗余的计算,简化了表达式,并减少了变量的使用,从而提高了代码的效率。需要注意的是,这些优化假设没有副作用和别名问题(即数组 A
在此期间不会被修改,且 j
是一个不变的值)。