認識 LLVM

張凱強發表於2022-02-26

簡介

LLVM是一套提供編譯器基礎設施的開源專案,是用 C++ 編寫,包含一系列模組化的編譯器元件和工具鏈,用來開發編譯器前端和後端。它是為了任意一種程式語言而寫成的程式,利用虛擬技術創造出編譯時期、連結時期、執行時期以及“閒置時期”的優化。

LLVM的命名源自於底層虛擬機器(Low Level Virtual Machine)的首字母縮寫,導致不瞭解它的人以為它是類似於 JVM(Java Virtual Machine) 的虛擬機器,實際上這個專案的範圍並不侷限於建立一個虛擬機器,而是包括 LLVM 中介碼(LLVM IR)、LLVM除錯工具、LLVM C++ 標準庫等一系列編譯工具及低端工具技術的集合。

傳統的靜態編譯器設計是三階段設計,其主要元件是前端、優化器和後端。

傳統的靜態編譯器設計

前端負責詞法分析、語法分析、語義分析、生成中間程式碼等功能。
優化器負責進行各種轉換以嘗試提高程式碼的執行時間,例如消除冗餘計算,並且通常或多或少獨立於語言和目標。
後端(也稱為程式碼生成器)負責將程式碼對映到目標指令集。除了編寫正確的程式碼外,它還負責生成利用所支援架構的不尋常特性的良好程式碼。編譯器後端的常見部分包括指令選擇、暫存器分配和指令排程。

該模型同樣適用於直譯器和 JIT 編譯器。JVM 也是該模型的一個實現,它使用 Java 位元組碼作為前端和優化器之間的介面。

而 LLVM 被設計為支援多種源語言或目標架構,它提供了一套適合編譯器系統的中間語言,如果編譯器在其優化器中使用這個中間語言表示,則可以為任何可以編譯到它的語言編寫前端,並且可以為任何可以從它編譯的目標編寫後端。

LLVM 架構設計

使用這種設計,移植編譯器以支援新的源語言只需要實現新的前端,即可以重用現有的優化器和後端;同樣想增加支援新的目標架構也只需要實現新的後端。而如果按傳統設計,前端和後端實際是耦合在一起,實現新的源語言或支援新的目標架構將需要從頭開始,要支援 N 目標和 M 源語言將需要 N*M 個編譯器。

LLVM IR

LLVM提供了一套適合編譯器系統的中間語言(Intermediate Representation,IR),有大量變換和優化都圍繞其實現,經過變換和優化後的中間語言,可以轉換為目標平臺相關的組合語言程式碼。

該中間語言與具體的語言、指令集、型別系統無關,其中每條指令都是靜態單賦值形式(SSA), 即每個變數只能被賦值一次。這有助於簡化變數之間的依賴分析。

以下是簡單的 LLVM IR 程式碼:

define i32 @add1(i32 %a, i32 %b) {
entry:
  %tmp1 = add i32 %a, %b
  ret i32 %tmp1
}

define i32 @add2(i32 %a, i32 %b) {
entry:
  %tmp1 = icmp eq i32 %a, 0
  br i1 %tmp1, label %done, label %recurse

recurse:
  %tmp2 = sub i32 %a, 1
  %tmp3 = add i32 %b, 1
  %tmp4 = call i32 @add2(i32 %tmp2, i32 %tmp3)
  ret i32 %tmp4

done:
  ret i32 %b
}

上述程式碼對應的 C 語言程式碼為:

unsigned add1(unsigned a, unsigned b) {
  return a+b;
}

unsigned add2(unsigned a, unsigned b) {
  if (a == 0) return b;
  return add2(a-1, b+1);
}

從這個例子可以看出,LLVM IR 是一種強型別的精簡指令集( RISC )。像真正的 RISC 指令集一樣,它支援簡單指令的線性序列,如加法、減法、比較和分支。這些指令採用三地址形式,這意味著它們接受一定數量的輸入並在不同的暫存器中產生結果。LLVM IR 支援標籤,通常看起來像一種奇怪的組合語言形式。

與大多數 RISC 指令集不同,LLVM 使用簡單的型別系統進行強型別化(例如,i32 是一個 32 位整數,i32** 是一個指向 32 位整數的指標),並且機器的一些細節被抽象掉了。例如,呼叫約定是通過指令和顯式引數 call 抽象出來的。ret 與機器程式碼的另一個顯著區別是 LLVM IR 不使用一組固定的命名暫存器,它使用一組無限的以 % 字元命名的臨時暫存器。

LLVM IR 支援三種表達形式:人類可讀的彙編、在C++中物件形式、序列化後的 bitcode 形式。

編譯

LLVM允許程式碼被靜態的編譯,包含在傳統的GCC系統底下,者通過實時編譯(JIT)機制將中間表示轉換為機器碼(類似 Java)。

LLVM 型別系統包含基本型別(整數或是浮點數)及五個複合型別(指標、陣列、向量、結構及函式),在LLVM具體語言的型別建制可以以結合基本型別來表示,舉例來說,C++所使用的class可以被表示為結構、函式及函式指標的陣列所組成。

LLVM 提供了 Clang 作為官方的編譯器前端,同時支援 C、C++、Objective-C 和 Objective-C++ 語言。主要來自 Apple 公司的贊助支援,Clang 的目的用以取代 GCC 系統底下的 C / Objective-C 編譯器,在當代的系統,它較為容易與整合開發環境(IDE)整合,而且對於執行緒有更好的支援。許多 GCC 的前端也已經可以與其執行,LLVM目前支援 Ada、C語言、C++、D語言、Fortran、Haskell、Julia、Objective-C、Rust 及 Swift 等語言的編譯。