編譯器(compiler)就是一個翻譯其他程序的程序而已。傳統(tǒng)的編譯器將源代碼翻譯為計算機(jī)能夠理解的可執(zhí)行機(jī)器代碼(有一些編譯器將源代碼翻譯為另一種編程語言薛窥。這些編譯器叫做從源碼到源碼的翻譯器,source-to-source translators or transpilers)。LLVM 是一個廣泛使用的編譯器項(xiàng)目膝晾,它包含了許多模塊化的編譯器工具栓始。傳統(tǒng)編譯器涉及包含了三個部分:
前端(frontend)將源代碼翻譯為一個中間表示(intermediate representation, IR)务冕。
clang
是 LLVM 中 C 系語言的前端。優(yōu)化器(optimizer)會對 IR 進(jìn)行分析幻赚,并將其翻譯成一個更高效的形式禀忆。
opt
是 LLVM 的優(yōu)化器工具。后端(backend)通過將 IR 映射為目標(biāo)硬件的指令集生成機(jī)器碼落恼。
llc
是 LLVM 的后端工具箩退。
LLVM IR 是一個類似匯編語言的低級語言。但是佳谦,它將針對特定硬件的信息抽象了出去戴涝。
Hello, Compiler
下面是一個簡單的 C 程序,它只是向標(biāo)準(zhǔn)輸出打印出 “Hello, Compiler!”钻蔑。雖然人類可以讀懂 C 語言的語法啥刻,但是機(jī)器并不認(rèn)識它。我將通過三個編譯步驟咪笑,使得機(jī)器能夠執(zhí)行這個程序可帽。
// compile_me.c
// Wave to the compiler. The world can wait.
#include <stdio.h>
int main() {
printf("Hello, Compiler!\n");
return 0;
}
The Frontend
正如我上面提到的,clang
是 LLVM C 系語言的前端窗怒。clang 包含了一個 C 預(yù)處理器(preprocessor)映跟,詞法分析器(lexer),語法分析器(parser),semantic analyzer(語義分析器)和中間表示生成器(IR generator)蓄拣。
-
C 預(yù)處理器 在翻譯成 IR 之前對源代碼進(jìn)行修改。預(yù)處理器會將外部文件包含進(jìn)來努隙,比如上面的
#include <stdio.h>
球恤。它會用 C 標(biāo)準(zhǔn)庫文件stdio.h
的所有內(nèi)容替換#include <stdio.h>
這一行,stdio.h
包含了printf
函數(shù)的聲明荸镊。
通過執(zhí)行下列命令來查看預(yù)處理器步驟的輸出:
clang -E compile_me.c -o preprocessed.i
- 詞法分析器(lexer, 或者叫 scanner 或 tokenizer) 將一串字符轉(zhuǎn)換成一串詞碎捺。每個詞,或者叫 token (記號)贷洲,被分配到 5 個句法的類別:punctuation(標(biāo)點(diǎn)符號)收厨,keyword(關(guān)鍵詞),identifier(標(biāo)識符)优构,literal(常量) 或 comment(注釋)诵叁。
compile_me.c 的 tokenization:
- 語法分析器決定了由詞法分析器生成的一串詞是否包含了源語言中的有效句。在分析完詞的語法以后钦椭,它輸出了一個抽象語法樹(abstract syntax tree, AST)拧额。一個 Clang AST 中的節(jié)點(diǎn)表示 declaration,statement, type.
compile_me.c 的 AST:
語義分析器遍歷 AST彪腔,判定代碼句的涵義是否有效侥锦。這個階段會檢查類型錯誤。如果 compile_me.c 中的 main 函數(shù)返回了
"zero"
而不是0
, 語義分析器就會拋出一個錯誤德挣,因?yàn)?"zero"
不是int
類型恭垦。IR 生成器 將 AST 翻譯為 IR。
在 compile_me.c 上運(yùn)行 clang 前端來生成 LLVM IR:
clang -S -emit-llvm -o llvm_ir.ll compile_me.c
在 llvm_ir.ll 中的 main 函數(shù):
; llvm_ir.ll
@.str = private unnamed_addr constant [18 x i8] c"Hello, Compiler!\0A\00", align 1
define i32 @main() {
%1 = alloca i32, align 4 ; <- memory allocated on the stack
store i32 0, i32* %1, align 4
%2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str, i32 0, i32 0))
ret i32 0
}
declare i32 @printf(i8*, ...)
優(yōu)化器
優(yōu)化器的任務(wù)是格嗅,基于對程序運(yùn)行時行為的理解番挺,提升代碼的效率。優(yōu)化器的輸入為 IR屯掖,輸出為優(yōu)化后的 IR玄柏。LLVM 的優(yōu)化器工具,opt
贴铜,將會使用 -O2
(大寫字母 o粪摘,2)標(biāo)志優(yōu)化處理器速度,-Os
(大寫字母 o绍坝,s)優(yōu)化生成目標(biāo)的大小徘意。
來看一下優(yōu)化器優(yōu)化之前的 LLVM IR 代碼和優(yōu)化后的代碼:
opt -O2 llvm_ir.ll -o optimized.ll
optimized.ll 的 main 函數(shù):
; optimized.ll
@str = private unnamed_addr constant [17 x i8] c"Hello, Compiler!\00"
define i32 @main() {
%puts = tail call i32 @puts(i8* getelementptr inbounds ([17 x i8], [17 x i8]* @str, i64 0, i64 0))
ret i32 0
}
declare i32 @puts(i8* nocapture readonly)
在優(yōu)化后的版本中,main 沒有在棧(stack)上分配內(nèi)存陷嘴,因?yàn)樗鼪]有使用任何內(nèi)存映砖。優(yōu)化后的代碼也沒有調(diào)用 printf
, 而是調(diào)用了 puts
,因?yàn)樗鼪]有用到 printf
的任何格式化功能灾挨。
當(dāng)然了邑退,優(yōu)化器知道的不僅僅是什么時候該用 puts
代替 printf
. 優(yōu)化器也會對循環(huán)進(jìn)行展開竹宋,內(nèi)聯(lián)簡單計算的結(jié)果〉丶迹考慮下面的代碼蜈七,它將兩個數(shù)加起來并打印結(jié)果:
// add.c
#include <stdio.h>
int main() {
int a = 5, b = 10, c = a + b;
printf("%i + %i = %i\n", a, b, c);
}
這是未優(yōu)化的 LLVM IR:
@.str = private unnamed_addr constant [14 x i8] c"%i + %i = %i\0A\00", align 1
define i32 @main() {
%1 = alloca i32, align 4 ; <- allocate stack space for var a
%2 = alloca i32, align 4 ; <- allocate stack space for var b
%3 = alloca i32, align 4 ; <- allocate stack space for var c
store i32 5, i32* %1, align 4 ; <- store 5 at memory location %1
store i32 10, i32* %2, align 4 ; <- store 10 at memory location %2
%4 = load i32, i32* %1, align 4 ; <- load the value at memory address %1 into register %4
%5 = load i32, i32* %2, align 4 ; <- load the value at memory address %2 into register %5
%6 = add nsw i32 %4, %5 ; <- add the values in registers %4 and %5. put the result in register %6
store i32 %6, i32* %3, align 4 ; <- put the value of register %6 into memory address %3
%7 = load i32, i32* %1, align 4 ; <- load the value at memory address %1 into register %7
%8 = load i32, i32* %2, align 4 ; <- load the value at memory address %2 into register %8
%9 = load i32, i32* %3, align 4 ; <- load the value at memory address %3 into register %9
%10 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0), i32 %7, i32 %8, i32 %9)
ret i32 0
}
declare i32 @printf(i8*, ...)
這是優(yōu)化后的 LLVM IR:
@.str = private unnamed_addr constant [14 x i8] c"%i + %i = %i\0A\00", align 1
define i32 @main() {
%1 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0), i32 5, i32 10, i32 15)
ret i32 0
}
declare i32 @printf(i8* nocapture readonly, ...)
優(yōu)化后的 main 函數(shù),本質(zhì)上就是未優(yōu)化版本的 17 和 18 行將變量進(jìn)行內(nèi)聯(lián)莫矗。opt
對加法進(jìn)行了計算飒硅,因?yàn)樗械淖兞慷际浅A俊:芸嶙餮瑁前桑?/p>
The Backend
LLVM 的后端工具是 llc
.從 LLVM IR 輸入生成機(jī)器碼三娩,它經(jīng)歷了三個階段:
指令選取(instruction selection) 是從 IR 指令到目標(biāo)機(jī)器指令集的映射妹懒。這一步使用了虛擬寄存器一個無限的命令空間雀监。
寄存器分配(register allocation) 是從虛擬寄存器到目標(biāo)架構(gòu)上真實(shí)寄存器的映射。我的 CPU 是 x86 架構(gòu)眨唬,也就是說只能使用 16 個寄存器会前。但是,編譯器會選擇盡可能少地使用寄存器匾竿。
指令調(diào)度(instruction scheduling) 是對操作的重新安排瓦宜,它反映了目標(biāo)機(jī)器上的性能限制。
執(zhí)行下面的命令將會產(chǎn)生一些機(jī)器碼岭妖!
llc -o compiled-assembly.s optimized.ll
_main:
pushq %rbp
movq %rsp, %rbp
leaq L_str(%rip), %rdi
callq _puts
xorl %eax, %eax
popq %rbp
retq
L_str:
.asciz "Hello, Compiler!"
這個程序是 x86 匯編語言临庇,它是目標(biāo)機(jī)器能夠讀懂的語言的一個“人類表示”。目標(biāo)機(jī)器只能讀懂 0 和 1区转,匯編語言是將 0 1 代碼用人類能夠讀懂的方式表達(dá)了出來苔巨。相信肯定會有人懂的:).
資源: