10. Kaleidoscope:結論和其他有用的 LLVM 技巧

10.1. 教學結論

歡迎來到「使用 LLVM 實作語言」教學的最後一章。在本教學課程中,我們已經將我們的小型 Kaleidoscope 語言從一個無用的玩具發展成一個半有趣的(但可能仍然無用)玩具。 :)

看看我們已經走了多遠,以及它所花費的程式碼量是很有趣的。我們構建了整個詞法分析器、解析器、AST、程式碼產生器、一個互動式執行迴圈(帶有 JIT!)以及在獨立可執行檔中發出的除錯資訊 - 所有這些都在 1000 行(非註解/非空白)程式碼內完成。

我們的小型語言支援一些有趣的功能:它支援使用者定義的二元和一元運算符,它使用 JIT 編譯進行立即求值,並且它支援一些具有 SSA 構造的控制流程構造。

本教學的一部分目的是向您展示定義、建置和使用語言是多麼容易和有趣。建置編譯器不一定是可怕或神秘的過程!既然您已經了解了一些基礎知識,我強烈建議您採用程式碼並進行修改。例如,嘗試新增

  • 全域變數 - 雖然全域變數在現代軟體工程中的價值值得懷疑,但在組合像 Kaleidoscope 編譯器本身這樣快速的小型駭客攻擊時,它們通常很有用。幸運的是,我們目前的設定使得新增全域變數變得非常容易:只需讓值查詢在拒絕未解析的變數之前檢查它是否在全域變數符號表中。若要建立新的全域變數,請建立 LLVM GlobalVariable 類別的執行個體。

  • 類型變數 - Kaleidoscope 目前僅支援 double 類型的變數。這給予語言一種非常優美的特性,因為只支援一種類型意味著您永遠不必指定類型。不同的語言有不同的處理方式。最簡單的方法是要求使用者為每個變數定義指定類型,並將變數的類型及其 Value* 記錄在符號表中。

  • 陣列、結構、向量等 - 新增類型後,您可以開始以各種有趣的方式擴充類型系統。簡單的陣列非常容易,並且對於許多不同的應用程式都非常有用。新增它們主要是學習 LLVM getelementptr 指令如何運作的練習:它是如此巧妙/非傳統,它有自己的常見問題解答

  • 標準執行時 - 我們目前的語言允許使用者存取任意的外部函式,我們將其用於像 “printd” 和 “putchard” 之類的功能。當您擴展語言以添加更高級別的構造時,如果這些構造被降低為對語言提供的執行時的呼叫,則通常最有意義。例如,如果您將雜湊表添加到語言中,將例程添加到執行時可能是有意義的,而不是一直將它們內聯。

  • 記憶體管理 - 目前我們只能在 Kaleidoscope 中存取堆疊。能夠配置堆記憶體也很有用,可以使用對標準 libc malloc/free 介面的呼叫或使用垃圾收集器。如果您想使用垃圾收集,請注意 LLVM 完全支援 精確垃圾收集,包括移動物件和需要掃描/更新堆疊的演算法。

  • 異常處理支援 - LLVM 支援產生 零成本異常,這些異常可以與以其他語言編譯的程式碼互通。您也可以透過隱式地使每個函式都返回一個錯誤值並檢查它來產生程式碼。您還可以顯式地使用 setjmp/longjmp。這裡有很多不同的方法。

  • 物件導向、泛型、資料庫存取、複數、幾何程式設計…… - 真的,您可以為語言添加的瘋狂功能數不勝數。

  • 不尋常的領域 - 我們一直在談論將 LLVM 應用於許多人感興趣的領域:為特定語言建構編譯器。然而,還有許多其他領域可以使用編譯器技術,而這些領域通常不被考慮。例如,LLVM 已被用於實現 OpenGL 圖形加速、將 C++ 程式碼轉換為 ActionScript,以及許多其他有趣且聰明的事情。也許您將是第一個使用 LLVM 將正規表示式解譯器 JIT 編譯為原生程式碼的人?

玩得開心 - 嘗試做一些瘋狂和不尋常的事情。像其他人一樣建構一種語言,遠不如嘗試一些有點瘋狂或不切實際的事情,看看結果如何來得有趣。如果您遇到困難或想談談它,請在 LLVM 論壇 上發帖:論壇裡有很多對語言感興趣的人,他們通常願意提供幫助。

在結束本教學之前,我想談談產生 LLVM IR 的一些“技巧和竅門”。這些是一些可能不太明顯但非常有用的微妙之處,如果您想利用 LLVM 的功能。

10.2. LLVM IR 的屬性

我們對 LLVM IR 形式的程式碼有一些常見問題 - 我們現在就把這些問題解決掉,好嗎?

10.2.1. 目標獨立性

Kaleidoscope 是一種“可移植語言”的例子:任何用 Kaleidoscope 編寫的程式在任何執行它的目標上都以相同的方式工作。許多其他語言都具有此屬性,例如 lisp、java、haskell、javascript、python 等(請注意,雖然這些語言是可移植的,但並非所有庫都是可移植的)。

LLVM 的一個優點是它通常能夠在 IR 中保持目標獨立性:您可以獲取 Kaleidoscope 編譯程式的 LLVM IR,並在 LLVM 支援的任何目標上執行它,甚至發出 C 程式碼並在 LLVM 本身不支援的目標上編譯它。您可以輕易地看出 Kaleidoscope 編譯器產生了目標獨立的程式碼,因為它在產生程式碼時從不查詢任何目標特定的資訊。

LLVM 提供了一種緊湊、目標平台無關的程式碼表示方式,讓許多人感到興奮。可惜的是,這些人在詢問有關語言可攜性問題時,通常想到的是 C 語言或 C 語言家族。我說「可惜」,是因為除了發佈原始碼之外,實際上沒有辦法讓(完全通用的)C 程式碼具有可攜性(當然,C 原始碼本身通常也不具有可攜性——試想將一個非常古老的應用程式從 32 位元移植到 64 位元?)。

C 語言(同樣,在其完全通用的情況下)的問題在於,它充斥著針對目標平台的假設。舉一個簡單的例子,預處理器在處理輸入文字時,經常會破壞性地消除程式碼的目標平台獨立性。

#ifdef __i386__
  int X = 1;
#else
  int X = 42;
#endif

雖然我們可以設計出越來越複雜的解決方案來解決這類問題,但無法以一種比發佈實際原始碼更好的方式來完全解決這個問題。

話雖如此,C 語言中確實存在一些可以實現可攜性的有趣子集。如果您願意將基本類型固定為固定大小(例如 int = 32 位元,long = 64 位元),不關心與現有二進制文件的 ABI 相容性,並且願意放棄其他一些次要功能,那麼您就可以擁有可攜式的程式碼。這對於專門的領域(例如核心內語言)來說是有意義的。

10.2.2. 安全保證

上述許多語言也是「安全的」語言:用 Java 編寫的程式不可能破壞其地址空間並導致程式崩潰(假設 JVM 沒有錯誤)。安全性是一個有趣的特性,它需要語言設計、執行時支援以及作業系統支援的結合。

當然可以在 LLVM 中實現一種安全的語言,但 LLVM IR 本身並不保證安全性。LLVM IR 允許不安全的指標轉換、釋放後使用錯誤、緩衝區溢位以及各種其他問題。安全性需要作為 LLVM 之上的一層來實現,而且方便的是,已經有幾個團隊對此進行了研究。如果您有興趣瞭解更多細節,請在 LLVM 論壇 上提問。

10.2.3. 語言特定的優化

LLVM 讓許多人卻步的一點是,它並沒有在一个系統中解決世界上所有的問題。一個具體的抱怨是,人們認為 LLVM 無法執行高階語言特定的優化:LLVM「丟失了太多資訊」。以下是一些關於此的觀察:

首先,您說得對,LLVM 確實會丟失資訊。例如,在撰寫本文時,LLVM IR 中沒有辦法區分 SSA 值是來自 C 語言的「int」還是來自 ILP32 機器上的 C 語言「long」(除了除錯資訊)。兩者都被編譯成「i32」值,並且關於其來源的資訊會丟失。這裡更普遍的問題是,LLVM 類型系統使用「結構等價」而不是「名稱等價」。另一個讓大家感到意外的地方是,如果高階語言中的兩個類型具有相同的結構(例如,兩個不同的結構都有一個 int 欄位):這些類型將被編譯成一個單一的 LLVM 類型,並且無法判斷其來源。

第二,雖然 LLVM 會遺失資訊,但 LLVM 並非固定目標:我們持續以各種方式強化和改進它。除了新增功能(LLVM 並非一直都支援例外或偵錯資訊),我們也擴充 IR 來擷取重要的資訊以供最佳化(例如引數是符號擴充還是零擴充、有關指標別名化等的資訊)。許多強化功能都是由使用者驅動的:人們希望 LLVM 包含某些特定功能,因此他們便著手進行擴充。

第三,新增語言特定的最佳化是*可行且容易的*,而且您有多種選擇。舉一個簡單的例子,新增「瞭解」以某種語言編譯的程式碼的語言特定最佳化過程很容易。在 C 語系的情況下,有一個最佳化過程「瞭解」標準 C 函式庫函式。如果您在 main() 中呼叫「exit(0)」,它會知道將其最佳化為「return 0;」是安全的,因為 C 規定了「exit」函式的作用。

除了簡單的函式庫知識外,也可以將各種其他語言特定資訊嵌入 LLVM IR 中。如果您有特定需求並遇到瓶頸,請在 llvm-dev 郵件清單上提出該主題。在最壞的情況下,您始終可以將 LLVM 視為「簡易程式碼產生器」,並在前置處理器中、在語言特定的 AST 上實作您想要的高階最佳化。

10.3. 秘訣與技巧

在使用/處理 LLVM 之後,您會瞭解到各種有用的秘訣與技巧,這些技巧乍看之下並不明顯。為了避免讓每個人都重新探索它們,本節將討論其中一些問題。

10.3.1. 實作可移植的 offsetof/sizeof

如果您嘗試讓編譯器產生的程式碼「與目標無關」,則會遇到一個有趣的情況,那就是您經常需要知道某些 LLVM 類型的尺寸,或 llvm 結構中某些欄位的位移。例如,您可能需要將類型的尺寸傳遞給配置記憶體的函式。

遺憾的是,這在不同目標之間可能會有很大的差異:例如,指標的寬度顯然會因目標而異。但是,有一種使用 getelementptr 指令的巧妙方法,可讓您以可移植的方式計算出來。

10.3.2. 垃圾回收堆疊框架

某些語言想要明確地管理其堆疊框架,通常是為了讓它們被垃圾回收或更容易實作閉包。通常有比明確的堆疊框架更好的方法來實作這些功能,但LLVM 確實支援它們,如果您需要的話。它需要您的前置處理器將程式碼轉換為續延傳遞風格,並使用尾端呼叫(LLVM 也支援)。