FaultMaps 和隱含檢查

動機

由託管語言運行時生成的代碼,往往會有為了安全而需要的檢查,但在實踐中永遠不會失敗。在這種情況下,即使失敗的情況變得更加昂貴,使非失敗的情況更便宜也是有利可圖的。可以通過將這些安全檢查折疊到可以可靠地故障的操作中來利用這種不對稱性(如果檢查會失敗),並通過使用信號處理程序從這種故障中恢復。

例如,Java 要求在讀取或寫入物件之前,必須先進行空值檢查。如果物件是 null,則必須拋出 NullPointerException,中斷正常執行。然而在實踐中,在行為良好的 Java 程式中,對 null 指標進行解引用極為罕見,通常空值檢查可以摺疊到附近對相同記憶體位置進行操作的記憶體操作中。

Fault Map 區段

關於 LLVM 生成的隱含檢查的資訊被放置在一個特殊的 “fault map” 區段中。在 Darwin 上,這個區段被命名為 __llvm_faultmaps

此區段的格式為

Header {
  uint8  : Fault Map Version (current version is 1)
  uint8  : Reserved (expected to be 0)
  uint16 : Reserved (expected to be 0)
}
uint32 : NumFunctions
FunctionInfo[NumFunctions] {
  uint64 : FunctionAddress
  uint32 : NumFaultingPCs
  uint32 : Reserved (expected to be 0)
  FunctionFaultInfo[NumFaultingPCs] {
    uint32  : FaultKind
    uint32  : FaultingPCOffset
    uint32  : HandlerPCOffset
  }
}

FailtKind 描述了預期故障的原因。目前支援三種類型的故障

  1. FaultMaps::FaultingLoad - 由於從記憶體載入而導致的故障。

  2. FaultMaps::FaultingLoadStore - 由於指令的載入和儲存而導致的故障。

  3. FaultMaps::FaultingStore - 由於儲存到記憶體而導致的故障。

ImplicitNullChecks Pass

ImplicitNullChecks Pass 轉換了用於檢查指標是否為 null 的顯式控制流程,例如

  %ptr = call i32* @get_ptr()
  %ptr_is_null = icmp i32* %ptr, null
  br i1 %ptr_is_null, label %is_null, label %not_null, !make.implicit !0

not_null:
  %t = load i32, i32* %ptr
  br label %do_something_with_t

is_null:
  call void @HFC()
  unreachable

!0 = !{}

轉換為指令載入或儲存時隱含的控制流程(通過正在進行空值檢查的指標)

  %ptr = call i32* @get_ptr()
  %t = load i32, i32* %ptr  ;; handler-pc = label %is_null
  br label %do_something_with_t

is_null:
  call void @HFC()
  unreachable

此轉換發生在 MachineInstr 層級,而不是 LLVM IR 層級(因此上面的範例僅是代表性的,而非字面意義)。如果將 -enable-implicit-null-checks 傳遞給 llcImplicitNullChecks Pass 會在代碼生成期間運行。

ImplicitNullChecks Pass 會根據需要將條目添加到上面描述的 __llvm_faultmaps 區段。

make.implicit 元數據

將空值檢查隱含化是一種激進的優化,如果過多的記憶體操作最終因此而發生故障,則可能會導致淨效能下降。語言運行時通常需要確保,一旦應用程式達到穩定狀態,只有極少數的隱含空值檢查實際上會發生故障。一種標準的做法是通過代碼修補或重新編譯,將失敗的隱含空值檢查修復為顯式空值檢查。由此可見,一個顯式空值檢查需要滿足兩個要求,才能將其轉換為隱含空值檢查是有利可圖的

  1. 指標實際上為 null 的情況(即“失敗”情況)極為罕見。

  2. 失敗路徑將隱含空值檢查修復為顯式空值檢查,以使應用程式不會重複發生頁面錯誤。

預期前端會使用 !make.implicit 元數據節點(元數據節點的實際內容將被忽略)來標記滿足 (1) 和 (2) 的分支。只有標記有 !make.implicit 元數據的分支才被視為轉換為隱含空值檢查的候選者。

(請注意,雖然我們可以使用分析數據來處理 (1),但處理 (2) 需要一些分支分析中不存在的資訊。)