不透明指標¶
不透明指標類型¶
傳統上,LLVM IR 指標類型包含一個指向類型。例如,i32*
是一個指向記憶體中某處的 i32
的指標。然而,由於缺乏指向類型語義以及擁有指向類型所帶來的各種問題,因此希望從指標中移除指向類型。
不透明指標類型專案旨在用不透明指標類型替換 LLVM 中所有包含指向類型的指標類型。新的指標類型以文字表示為 ptr
。
某些指令仍然需要知道要將指標指向的記憶體視為何種類型。例如,載入指令需要知道要從記憶體中載入多少位元組,以及將結果值視為何種類型。在這些情況下,指令本身包含一個類型參數。例如,舊版 LLVM 中的載入指令
load i64* %p
變成
load i64, ptr %p
地址空間仍然用於區分不同種類的指標,其中區分對於降低層級很重要(例如,數據指標和函數指標在某些架構上具有不同的大小)。不透明指標不會改變與地址空間和降低層級相關的任何內容。如需更多資訊,請參閱 DataLayout。非預設地址空間中的不透明指標拼寫為 ptr addrspace(N)
。
這早在 2015 年 就被提出了。
顯式指向類型的問題¶
LLVM IR 指標可以在具有不同指向類型的指標之間來回轉換。指向類型不一定代表記憶體中實際的底層類型。換句話說,指向類型沒有真正的語義。
從歷史上看,LLVM 是 C 語言某種类型安全的子集。擁有指向類型提供了一層額外的檢查,以確保 Clang 前端能將其前端值/操作與相應的 LLVM IR 相匹配。然而,隨著 C++ 等其他語言採用 LLVM,社群意識到指向類型更多地阻礙了 LLVM 的發展,並且某些前端的額外類型檢查並不值得。
LLVM 的類型系統 最初設計 用於支持高級優化。然而,多年的 LLVM 實現經驗表明,指向類型系統設計並不能有效地支持優化。記憶體優化演算法,如 SROA、GVN 和 AA,通常需要查看 LLVM 的結構類型並推斷底層記憶體偏移量。社群意識到指向類型阻礙了 LLVM 的發展,而不是幫助它。由於通過 SSA 值直接表示高級語言資訊的限制,一些最初提出的高級優化已經演變成 TBAA。
指標類型為前端提供了一些價值,因為 IR 驗證器使用類型來偵測直接的類型混淆錯誤。但是,前端也必須處理在任何可能需要的地方插入位元轉換的複雜性。社群的共識是,指標類型的成本超過了效益,應該將其移除。
許多操作實際上並不在乎底層類型。這些操作(通常是內建函數)通常最終會採用任意指標類型 i8*
,有時還會採用大小。這會導致 IR 中有許多從具有不同指標類型的指標來回轉換的多餘無操作位元轉換。
無操作位元轉換會佔用記憶體/磁碟空間,並且需要花費編譯時間來查看。但是,最大的問題可能是處理位元轉換所需的程式碼複雜性。在透過定義-使用鏈向上查找指標時,很容易忘記呼叫 Value::stripPointerCasts() 來查找被位元轉換遮蔽的真實底層指標。而在透過定義-使用鏈向下查找時,遍歷需要迭代位元轉換來處理使用。移除無操作指標位元轉換可以防止一種類型的最佳化遺漏,並使編寫 LLVM 遍歷變得更容易一些。
較少的無操作指標位元轉換還可以降低地址空間中出現不正確位元轉換的機會。非常關心地址空間的後端維護者抱怨,像 Clang 這樣的編譯前端經常錯誤地對指標進行位元轉換,從而丟失地址空間資訊。
LLVM 早期發生的類似轉換是整數符號性。目前,有符號和無符號整數類型之間沒有區別,而是每個整數操作(例如加法)都包含用於指示如何處理整數的標記。以前,LLVM IR 會區分無符號和有符號整數類型,並且遇到了類似的無操作轉換問題。從在類型中體現符號性到指令的轉換發生在 LLVM 時間線的早期,目的是使 LLVM 更易於使用。
不透明指標模式¶
在轉換階段,LLVM 可以以兩種模式使用:在類型指標模式下,所有指標類型都具有指標類型,並且不能使用不透明指標。在不透明指標模式(預設模式)下,所有指標都是不透明的。可以使用 LLVM 工具(如 opt
)中的 -opaque-pointers=0
或 clang 中的 -Xclang -no-opaque-pointers
來停用不透明指標模式。此外,對於明確提及 i8*
風格類型指標的 IR 和位元碼檔案,會自動停用不透明指標模式。
在不透明指標模式下,IR、位元碼中使用的所有類型指標或使用 PointerType::get()
和類似 API 建立的指標都會自動轉換為不透明指標。這簡化了遷移,並允許使用不透明指標測試現有 IR。
define i8* @test(i8* %p) {
%p2 = getelementptr i8, i8* %p, i64 1
ret i8* %p2
}
; Is automatically converted into the following if -opaque-pointers
; is enabled:
define ptr @test(ptr %p) {
%p2 = getelementptr i8, ptr %p, i64 1
ret ptr %p2
}
遷移說明¶
為了支援不透明指標,通常需要進行兩種类型的更改。第一種是移除對 PointerType::getElementType()
和 Type::getPointerElementType()
的所有呼叫。
在 LLVM 中間端和後端,這通常是透過檢查相關操作的類型來完成的。例如,與記憶體存取相關的分析和最佳化應該使用載入和儲存指令中編碼的類型,而不是查詢指標類型。
以下是避免指標元素類型存取的一些常用方法
對於載入,請使用
getType()
。對於儲存,請使用
getValueOperand()->getType()
。使用
getLoadStoreType()
可透過單一呼叫處理上述兩種情況。對於 getelementptr 指令,請使用
getSourceElementType()
。對於呼叫,請使用
getFunctionType()
。對於 alloca,請使用
getAllocatedType()
。對於全域變數,請使用
getValueType()
。對於一致性斷言,請使用
PointerType::isOpaqueOrPointeeTypeEquals()
。若要在不同的位址空間中建立指標型別,請使用
PointerType::getWithSamePointeeType()
。若要檢查兩個指標是否具有相同的元素型別,請使用
PointerType::hasSameElementTypeAs()
。雖然建議以接受型別化指標和不透明指標的方式編寫程式碼,但可以使用
Type::isOpaquePointerTy()
和PointerType::isOpaque()
來特別處理不透明指標。PointerType::getNonOpaquePointerElementType()
可用作已明確排除不透明指標的程式碼路徑中的標記。若要取得 byval 引數的型別,請使用
getParamByValType()
。其他需要知道元素型別的 ABI 影響屬性(例如 byref、sret、inalloca 和 preallocated)也存在類似的函式。某些內建函式需要
elementtype
屬性,可以使用getParamElementType()
來擷取。如果內建函式本身未編碼所需的元素型別,則需要此屬性。這也用於內嵌組譯。
請注意,上述某些函式僅用於同時支援型別化指標和不透明指標,並且在移轉完成後將會移除。例如,當所有指標都不透明時,isOpaqueOrPointeeTypeEquals()
將會失去意義。
雖然指標元素型別的直接使用在程式碼中顯而易見,但有一個更微妙的問題是不透明指標需要處理的:許多程式碼假設指標相等也意味著所使用的載入/儲存型別或 GEP 來源元素型別相同。請考慮以下使用型別化指標和不透明指標的範例
define i32 @test(i32* %p) {
store i32 0, i32* %p
%bc = bitcast i32* %p to i64*
%v = load i64, i64* %bc
ret i64 %v
}
define i32 @test(ptr %p) {
store i32 0, ptr %p
%v = load i64, ptr %p
ret i64 %v
}
如果沒有不透明指標,檢查載入和儲存的指標運算元是否相同也能確保存取的型別相同。使用不同的型別需要進行位元轉換,這會導致不同的指標運算元。
對於不透明指標,位元轉換不存在,因此此檢查不再足夠。在上述範例中,這可能會導致將錯誤型別轉送至載入儲存。做出此類假設的程式碼需要調整為明確檢查存取的型別:LI->getType() == SI->getValueOperand()->getType()
。
前端¶
前端需要調整為獨立於 LLVM 追蹤指標型別,只要它們對於降低層級是必要的。例如,clang 現在在 Address
結構中追蹤指標型別。
透過 FFI 介面使用 C API 的前端應該注意,許多 C API 函式已被取代,並將在不透明指標轉換過程中移除
LLVMBuildLoad -> LLVMBuildLoad2
LLVMBuildCall -> LLVMBuildCall2
LLVMBuildInvoke -> LLVMBuildInvoke2
LLVMBuildGEP -> LLVMBuildGEP2
LLVMBuildInBoundsGEP -> LLVMBuildInBoundsGEP2
LLVMBuildStructGEP -> LLVMBuildStructGEP2
LLVMBuildPtrDiff -> LLVMBuildPtrDiff2
LLVMConstGEP -> LLVMConstGEP2
LLVMConstInBoundsGEP -> LLVMConstInBoundsGEP2
LLVMAddAlias -> LLVMAddAlias2
此外,將不再能夠在指標型別上呼叫 LLVMGetElementType()
。
您可以使用 LLVMContext::setOpaquePointers
來控制是否使用不透明指針(如果您想覆蓋預設值)。
暫時停用不透明指針¶
在 LLVM 15 中,不透明指針預設為啟用,但仍然可以使用多個選用標誌來使用類型化指針。
對於 clang 驅動程序接口的用戶,可以使用 -DCLANG_ENABLE_OPAQUE_POINTERS=OFF
cmake 選項或將 -Xclang -no-opaque-pointers
傳遞給單個 clang 調用來臨時恢復舊的預設值。
對於 clang cc1 接口的用戶,可以傳遞 -no-opaque-pointers
。請注意,CLANG_ENABLE_OPAQUE_POINTERS
cmake 選項對 cc1 接口沒有影響。
可以通過將 -Wl,-plugin-opt=no-opaque-pointers
傳遞給 clang 驅動程序來停用 LTO 的使用。
對於將 LLVM 作為庫的用戶,可以通過在 LLVMContext
上調用 setOpaquePointers(false)
來停用不透明指針。
對於 LLVM 工具(如 opt)的用戶,可以通過傳遞 -opaque-pointers=0
來停用不透明指針。
版本支持¶
LLVM 14: 支持遷移到不透明指針所需的所有 API,並棄用/刪除了不兼容的 API。但是,不完全支持在優化流程中使用不透明指針。此版本可用於使樹外代碼與不透明指針兼容,但不應在生產環境中啟用不透明指針。
LLVM 15: 預設情況下啟用不透明指針。仍然支持類型化指針。
LLVM 16: 預設情況下啟用不透明指針。僅在盡力而為的基礎上支持類型化指針,並且未經測試。
LLVM 17: 僅支持不透明指針。不支持類型化指針。
過渡狀態¶
截至 2023 年 7 月
在 main
分支上不支持類型化指針。
已刪除以下類型化指針功能
不再支持
CLANG_ENABLE_OPAQUE_POINTERS
cmake 標誌。不再支持
-no-opaque-pointers
cc1 clang 標誌。不再支持
-opaque-pointers
opt 標誌。不再支持
-plugin-opt=no-opaque-pointers
LTO 標誌。不再支持不支持不透明指針的 C API(如
LLVMBuildLoad
)。
以下類型化指針功能仍將被刪除
與不透明指針不再相關的各種 API。