不透明指標

不透明指標類型

傳統上,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 中存在大量多餘的空操作位元轉換,往返於具有不同指向目標類型的指標。

空操作位元轉換佔用記憶體/磁碟空間,並且還佔用編譯時間來查看。然而,也許最大的問題是處理位元轉換所需的程式碼複雜性。當向上查看指標的 def-use 鏈時,很容易忘記呼叫 Value::stripPointerCasts() 來找到被位元轉換混淆的真實底層指標。當向下查看 def-use 鏈時,Pass 需要迭代位元轉換來處理用途。移除空操作指標位元轉換可以防止一類錯過的最佳化,並使編寫 LLVM Pass 變得稍微容易一些。

更少的空操作指標位元轉換也降低了關於位址空間的不正確位元轉換的機會。維護非常關心位址空間的後端的人員抱怨說,像 Clang 這樣的前端經常不正確地進行指標位元轉換,從而遺失位址空間資訊。

LLVM 中早期發生的類似轉換是整數符號。目前,有符號和無符號整數類型之間沒有區別,而是每個整數操作(例如,add)都包含標誌,以指示如何處理整數。以前,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()

  • 對於 allocas,使用 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 介面沒有影響。

LTO 的使用可以透過將 -Wl,-plugin-opt=no-opaque-pointers 傳遞給 clang 驅動程式來停用。

對於將 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。