編譯器入門:沒有siri的那些年,我們?nèi)绾螌?shí)現(xiàn)人機(jī)對(duì)話?
編譯器可將源代碼轉(zhuǎn)換成計(jì)算機(jī)理解的可執(zhí)行的機(jī)器代碼,或?qū)⒃创a轉(zhuǎn)換成另一種編程語言。本文從 LLVM 入手介紹了編譯器工具。
編譯器不過就是一個(gè)翻譯其它程序的程序。傳統(tǒng)的編譯器將源代碼轉(zhuǎn)換成計(jì)算機(jī)可理解的可執(zhí)行的機(jī)器代碼。(一些編譯器將源代碼轉(zhuǎn)換為另一種編程語言,這些編譯器被稱為源到源轉(zhuǎn)換器或轉(zhuǎn)譯器)。LLVM 是一個(gè)廣泛使用的編譯器項(xiàng)目,包括多個(gè)模塊化的編譯器工具。
傳統(tǒng)的編譯器設(shè)計(jì)包括三個(gè)部分:
- 前端將源代碼轉(zhuǎn)換成一種中間表示(IR)。clang (http://clang.llvm.org/) 是 LLVM 項(xiàng)目中 C 類語言的前端工具。
- 優(yōu)化器解析 IR 并將其轉(zhuǎn)換成一種更高效的形式。opt是 LLVM 項(xiàng)目的優(yōu)化器工具。
- 后端通過將 IR 映射到目標(biāo)硬件指令集上來生成機(jī)器代碼。llc 是 LLVM 項(xiàng)目的后端工具。
LLVM IR 是一種類似匯編的低級(jí)語言。但是,它不針對(duì)特定的硬件信息編程。
你好,編譯器
下面是一個(gè)簡單的打印「Hello,Compiler」字符串的 C 語言程序。雖然程序員可以讀懂 C 語言語法,但是計(jì)算機(jī)卻看的一臉懵逼。接下來我要過一遍編譯的三個(gè)階段,以便將以下程序轉(zhuǎ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;
- }
前端
前文講到,clang 是 LLVM C 類語言的前端工具。Clang 由一個(gè) C 預(yù)處理器、詞法分析器(lexer)、解析器、語義分析器和中間表示生成器組成。
C 預(yù)處理器在源代碼轉(zhuǎn)換成 IR 之前對(duì)其進(jìn)行修改。預(yù)處理器會(huì)將外部文件包含進(jìn)來,比如上面的 #include
- clang -E compile_me.c -o preprocessed.i
詞法分析器(Lexer,也叫 scanner 或 tokenizer)將一串字符轉(zhuǎn)換成一串詞。每個(gè)詞或符號(hào),按其屬性被分配到對(duì)應(yīng)的句法類別:標(biāo)點(diǎn)符號(hào)、關(guān)鍵詞、標(biāo)識(shí)符、常量或注釋。
compile_me.c 的詞法分析:
解析器判定由詞法分析器生成的一串詞是否包含源語言中的有效語句。在分析完詞的語法以后,解析器輸出了一個(gè)抽象語法樹(AST)。Clang AST 中的節(jié)點(diǎn)分別表示聲明與類型。
compile_me.c 的 AST:
語義分析器遍歷 AST,判定語句的涵義是否有效。這個(gè)階段會(huì)檢查類型錯(cuò)誤。如果 compile_me.c 中的 main 函數(shù)返回了 "zero" 而不是 0, 語義分析器就會(huì)拋出一個(gè)錯(cuò)誤,因?yàn)?"zero" 不是 int 類型。
IR 生成器將 AST 轉(zhuǎn)換為 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ù)是基于對(duì)程序運(yùn)行時(shí)行為的理解,提升代碼的效率。優(yōu)化器的輸入為 IR,輸出為優(yōu)化后的 IR。LLVM 的優(yōu)化器工具 opt 將使用 -O2(大寫字母 o,數(shù)字 2)標(biāo)記優(yōu)化處理器速度,使用-Os(大寫字母 o,s)標(biāo)記優(yōu)化生成目標(biāo)的大小。
看一下優(yōu)化器優(yōu)化之前的 LLVM IR 代碼和優(yōu)化后的代碼:
- opt -O2 -S 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 函數(shù)沒有在棧上分配內(nèi)存,因?yàn)樗鼪]有使用任何內(nèi)存。優(yōu)化后的代碼調(diào)用了 puts 函數(shù)而不是 printf 函數(shù),因?yàn)樗鼪]有使用 printf 函數(shù)的任何格式化功能。當(dāng)然了,優(yōu)化器不僅僅知道什么時(shí)候該用 puts 代替 printf。優(yōu)化器也會(huì)展開循環(huán),內(nèi)聯(lián)簡單計(jì)算的結(jié)果。思考以下代碼,它將兩個(gè)數(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ù)實(shí)際上就是在未優(yōu)化版本的 17 和 18 行將變量進(jìn)行內(nèi)聯(lián)。opt 對(duì)加法進(jìn)行運(yùn)算,因?yàn)樗械淖兞慷际浅A?。很酷?
后端
LLVM 的后端工具是 llc。它經(jīng)歷了三個(gè)階段,最終把 LLVM IR 輸入轉(zhuǎn)化生成機(jī)器代碼:
- 指令選取(instruction selection)是從 IR 指令到目標(biāo)機(jī)器指令集的映射。這一步使用了虛擬寄存器一個(gè)***的命名空間。
- 寄存器分配(register allocation)是從虛擬寄存器到目標(biāo)架構(gòu)真實(shí)寄存器的映射。我的 CPU 是 x86 架構(gòu)的,也就是說只能使用 16 個(gè)寄存器。但是,編譯器會(huì)盡可能少地使用寄存器。
- 指令調(diào)度(instruction scheduling)是對(duì)操作的重新安排,它反映了目標(biāo)機(jī)器上的性能限制。
執(zhí)行以下命令將生成部分機(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!"
這是一個(gè) x86 匯編語言程序,是計(jì)算機(jī)和程序員共通的語言??此苹逎隙ㄓ腥硕?。
原文:https://nicoleorchard.com/blog/compilers
【本文是51CTO專欄機(jī)構(gòu)“機(jī)器之心”的原創(chuàng)譯文,微信公眾號(hào)“機(jī)器之心( id: almosthuman2014)”】