10. 萬花筒:結論與其他有用的 LLVM 小技巧

10.1. 教學結論

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

有趣的是看到我們走了多遠,以及只用了這麼少的程式碼。我們建立了完整的詞法分析器、語法分析器、AST、程式碼產生器、互動式執行迴圈(帶有 JIT!),並在獨立可執行檔中發出除錯資訊 - 全部都在 1000 行(非註解/非空白)程式碼以下。

我們的小語言支援幾個有趣的功能:它支援使用者定義的二元和一元運算子,它使用 JIT 編譯進行即時求值,並且它支援一些帶有 SSA 建構的控制流程結構。

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

  • 全域變數 - 雖然全域變數在現代軟體工程中的價值令人質疑,但當組合像萬花筒編譯器本身這樣快速的小 hacks 時,它們通常很有用。幸運的是,我們目前的設定使新增全域變數非常容易:只需讓數值查找檢查未解析的變數是否在全域變數符號表中,然後再拒絕它。若要建立新的全域變數,請建立 LLVM GlobalVariable 類別的實例。

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

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

  • 標準執行時期 - 我們目前的語言允許使用者存取任意外部函式,我們將其用於諸如「printd」和「putchard」之類的操作。當您擴展語言以新增更高階的結構時,通常如果將這些結構降級為對語言提供的執行時期的呼叫,它們最有意義。例如,如果您向語言新增雜湊表,則將常式新增到執行時期而不是完全內聯它們可能更有意義。

  • 記憶體管理 - 目前我們只能在萬花筒中存取堆疊。如果能夠分配堆積記憶體也會很有用,可以使用對標準 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. 目標獨立性

萬花筒是「可移植語言」的一個範例:以萬花筒編寫的任何程式都將在它運行的任何目標上以相同的方式工作。許多其他語言都具有此屬性,例如 lisp、java、haskell、javascript、python 等(請注意,雖然這些語言是可移植的,但並非所有程式庫都是)。

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

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 值是來自 ILP32 機器上的 C “int” 還是 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 確實支援它們,如果您需要。它要求您的前端將程式碼轉換為 Continuation Passing Style 並使用尾部呼叫(LLVM 也支援)。