LLVM 不透明指標支援已實裝
即將推出的 Crystal 1.8 次要版本將首次支援 LLVM 的不透明指標,允許使用 LLVM 15 或更高版本建置編譯器。此外,此更新顯著改善了編譯時間。
LLVM 中的指標
為了理解不透明指標的重要性,讓我們看一個小的範例程式
# test.cr
class Foo
def initialize(@x : Int32)
end
end
Foo.new(1)
使用 crystal build --prelude=empty --no-debug --emit=llvm-ir test.cr
建置上述程式。編譯器將建立一個檔案 test.ll
,其中包含我們程式的 LLVM IR,LLVM 用來發出 LLVM 位元組碼並最終產生機器碼的平台獨立中繼表示法。以下是與 Foo#initialize
對應的 LLVM 函式
; Function Attrs: uwtable
define internal i32 @"*Foo#initialize<Int32>:Int32"(%Foo* %self, i32 %x) #0 {
entry:
%0 = getelementptr inbounds %Foo, %Foo* %self, i32 0, i32 1
store i32 %x, i32* %0, align 4
ret i32 %x
}
雖然不深入了解 Crystal 如何將方法編譯為此 LLVM 函式的細節(雖然我們過去確實有關於此的撰寫),但我們知道它
- 將
%self
參數(正在建構的Foo
物件)和來自Foo#initialize
的%x
參數作為引數; - 將
Foo
物件的@x
實例變數的位址指派給區域變數%0
; - 透過
%0
將%x
參數儲存到@x
中; - 將
%x
回傳給呼叫者。(此呼叫者通常是Foo.new
,因此大部分時間都不會使用。)
我們可以看到 %self
和 %0
的類型分別為 %Foo*
和 i32*
。這些是類型指標,LLVM 已經使用很長時間了。在 LLVM 中,不同指向類型的類型指標必須使用 bitcast
LLVM 指令明確轉換,否則產生的 LLVM IR 將格式錯誤。隨著越來越多的 LLVM 前端出現,人們很快意識到類型指標並沒有提供許多有用的語義,反而增加了 IR 產生和分析的不必要複雜性。
不透明指標
不透明指標最早是在 2015 年 2 月提出的,其中所有指標類型都將在 LLVM IR 中用單個 ptr
表示。然後在 2022 年 9 月,LLVM 15 現在預設使用不透明指標,而類型指標將在不久後移除。如果我們再次編譯上面的程式,但這次使用使用 LLVM 15 建置的 Crystal 編譯器,我們可以觀察不透明指標的作用
; Function Attrs: uwtable(sync)
define internal i32 @"*Foo#initialize<Int32>:Int32"(ptr %self, i32 %x) #0 {
entry:
%0 = getelementptr inbounds %Foo, ptr %self, i32 0, i32 1
store i32 %x, ptr %0, align 4
ret i32 %x
}
為了達到這個目的,必須根據使用的是類型指標還是不透明指標以不同的方式建置一些 LLVM 指令,而 getelementptr
指令就是其中一個例子。物件類型過去是從給定的指標推斷出來的,但現在不透明指標不攜帶該資訊,因此 IR 產生器(我們的 Crystal 編譯器)需要單獨提供此物件類型。在這種情況下,@x
實例變數和 #initialize
方法都屬於 Foo
,因此 Crystal 知道將 Foo
傳遞給 getelementptr
。但是這個指令在編譯器中的其他幾十個地方也使用了,沒有通用的遷移適用於這些地方。
Crystal 編譯器更新
遷移到不透明指標的工作於 2022 年 10 月開始,在 LLVM 15 發佈一個月後,經過無數次的區段錯誤和規格失敗,Crystal 現在在 master 分支上支援 LLVM 15。由於這是一項相當大的努力,我們當然希望不透明指標能夠帶來 LLVM 所承諾的效能優勢。因此,以下是一些從在 Apple M2 上重新建置編譯器本身時收集的數字,首先使用 LLVM 14 編譯器,然後使用 LLVM 15 編譯器
- 非發佈版本建置
- 程式碼產生 (crystal):2.65 秒 → 2.84 秒
- 程式碼產生 (bc+obj):5.91 秒 → 5.20 秒
- 程式碼產生 (連結):0.76 秒 → 0.34 秒
- dsymutil:0.35 秒 → 0.39 秒
- 發佈版本建置
- 程式碼產生 (bc+obj):247.86 秒 → 184.37 秒
- 程式碼產生 (連結):0.45 秒 → 0.33 秒
- dsymutil:0.63 秒 → 0.52 秒
如果我們只考慮最後 3 個完全由 LLVM 控制的階段,則非發佈版本的速度提升了 18%,而發佈版本的速度提升了 34%!渴望使用 LLVM 15 重新建置 Crystal 的開發人員報告了類似的數字。儘管為像 Crystal 這樣大的程式產生 LLVM IR 平均多花費 0.2 秒,但 LLVM 的改進幅度遠遠超過了它。這種遷移工作顯然得到了回報。
這對我有什么影響?
Crystal 的每晚建置已經在使用使用 LLVM 15 建置的編譯器,並且可以隨時嘗試。1.8 將是第一個使用 LLVM 15 建置的穩定版本。這些編譯器使用不透明指標,並顯示程式碼產生時間的改進。使用 LLVM 14 及更低版本建置的編譯器將繼續使用類型指標。
如果您的 Crystal 專案直接使用 stdlib 的 LLVM API,則需要注意一些棄用。否則,此變更不會以任何方式影響 Crystal 程式。它只會加快編譯器的速度。
另一方面,如果您確實使用 Crystal 來使用 Crystal 的 LLVM
API 建置其他 LLVM 前端,請注意,作為遷移的一部分,Crystal 將停止支援 LLVM 8.0 以下的版本,因為 8.0 是 LLVM 接受受不透明指標影響的指令(如上面的 getelementptr
)的單獨類型的第一個版本。所有依賴類型指標的功能都已棄用,無論是否實際使用了 LLVM 15。以下是受影響方法的完整清單
LLVM::Type#element_type
:在 LLVM 15 或更高版本上,在指標類型上呼叫此方法會引發例外狀況。LLVM::Function#function_type
、#return_type
、#varargs?
這些方法沒有快速遷移的方法。但是,如果函式是透過LLVM::FunctionCollection#add
建構的,則該方法現在有其他多載,可以直接採用 LLVM 函式類型。這允許您使用LLVM::Type.function
並在建構函式之前將類型儲存在其他地方。LLVM::Builder#call(func : LLVM::Function, ...)
、#invoke(fn : LLVM::Function, ...)
它們分別等同於call(func.function_type, func, ...)
和invoke(fn.function_type, fn, ...)
。請注意,#function_type
已棄用,並且在 LLVM 15+ 上無法運作。LLVM::Builder#load(ptr, ...)
這等同於load(ptr.type.element_type, ptr, ...)
。請注意,#element_type
將在 LLVM 15+ 上引發例外狀況。LLVM::Builder#gep(value, ...)
、LLVM::Builder#inbounds_gep(value, ...)
分別等同於gep(value.type, value, ...)
和inbounds_gep(value.type, value, ...)
。請注意,#type
在 LLVM 15+ 上僅是不透明的指標類型。
此外,即使 LLVM 15 提供了選擇性啟用類型指標支援的旗標,Crystal 完全未使用此旗標,這使得升級到 LLVM 16 及更高版本更加容易,因為 LLVM 最終將移除此旗標。