FaultMaps 和隐式检查

动机

由托管语言运行时生成的代码往往包含一些为了安全而必需但在实践中永远不会失败的检查。在这种情况下,即使使失败情况的代价显著增加,也有利于使非失败情况更便宜。可以通过将此类安全检查折叠到可以可靠地发生故障(如果检查失败)的操作中,并使用信号处理程序从这种故障中恢复,来利用这种不对称性。

例如,Java 要求在读取或写入对象之前对对象进行空值检查。如果对象为 null,则必须抛出 NullPointerException,从而中断正常的执行。然而,在行为良好的 Java 程序中,解除对 null 指针的引用非常罕见,并且通常可以将空值检查折叠到对同一内存位置进行操作的附近内存操作中。

故障映射区域

有关 LLVM 生成的隐式检查的信息被放置在一个特殊的“故障映射”区域中。在 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 传递

ImplicitNullChecks 传递将用于检查指针是否为 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 传递给 llc,则 ImplicitNullChecks 传递在代码生成期间运行。

ImplicitNullChecks 传递根据需要向上面描述的 __llvm_faultmaps 区域添加条目。

make.implicit 元数据

使空值检查隐式化是一种激进的优化,如果由于它导致过多的内存操作发生故障,则它可能导致净性能下降。语言运行时通常需要确保一旦应用程序达到稳定状态,只有极少数的隐式空值检查实际上会发生故障。一种标准的方法是通过代码修补或重新编译将失败的隐式空值检查修复为显式空值检查。因此,显式空值检查需要满足两个条件才能使其转换为隐式空值检查变得有利

  1. 指针实际上为 null 的情况(即“失败”情况)非常罕见。

  2. 失败路径将隐式空值检查修复为显式空值检查,以使应用程序不会重复发生页面错误。

预期前端使用 !make.implicit 元数据节点标记满足 (1) 和 (2) 的分支(元数据节点的实际内容被忽略)。只有标记有 !make.implicit 元数据的分支才会被视为转换为隐式空值检查的候选对象。

(请注意,虽然我们可以使用分析数据处理 (1),但处理 (2) 需要分支分析中不存在的一些信息。)