不透明指针

不透明指针类型

传统上,LLVM IR 指针类型包含一个被指向类型。例如,i32* 是一个指针,它指向内存中某处的 i32。然而,由于缺乏被指向类型语义以及拥有被指向类型的各种问题,人们希望从指针中移除被指向类型。

不透明指针类型项目旨在用不透明指针类型替换 LLVM 中所有包含被指向类型的指针类型。新的指针类型在文本上表示为 ptr

某些指令仍然需要知道将指针指向的内存视为哪种类型。例如,load 指令需要知道从内存加载多少字节以及将结果值视为哪种类型。在这些情况下,指令本身包含一个类型参数。例如,来自旧版本 LLVM 的 load 指令

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 验证器使用类型来检测直接的类型混淆错误。然而,前端也必须处理在可能需要的地方插入 bitcast 的复杂性。社区的共识是,被指向类型的成本超过了收益,并且应该将其移除。

许多操作实际上并不关心底层类型。这些操作,通常是 intrinsic,通常最终会接受任意指针类型 i8*,有时还会接受大小。这导致 IR 中存在大量冗余的无操作 bitcast,用于在具有不同被指向类型的指针之间进行转换。

无操作 bitcast 占用内存/磁盘空间,并且也占用编译时间来查找。然而,也许最大的问题是处理 bitcast 所需的代码复杂性。当在指针的 def-use 链中查找时,很容易忘记调用 Value::stripPointerCasts() 来找到被 bitcast 混淆的真正底层指针。当向下查看 def-use 链时,pass 需要迭代 bitcast 以处理 uses。移除无操作指针 bitcast 可以防止一类错过的优化,并使编写 LLVM pass 稍微容易一些。

更少的无操作指针 bitcast 也减少了地址空间方面不正确 bitcast 的机会。维护非常关注地址空间的后端的开发人员抱怨说,像 Clang 这样的前端经常错误地 bitcast 指针,丢失地址空间信息。

LLVM 中早期发生的类似转变是整数有符号性。目前,有符号和无符号整数类型之间没有区别,而是每个整数操作(例如 add)都包含标志来指示如何处理整数。以前,LLVM IR 区分无符号和有符号整数类型,并且遇到了类似的无操作 cast 问题。从在类型中体现有符号性到指令的转变发生在 LLVM 时间线的早期,以使 LLVM 更易于使用。

不透明指针模式

在过渡阶段,LLVM 可以以两种模式使用:在类型化指针模式下,所有指针类型都具有被指向类型,并且不能使用不透明指针。在不透明指针模式(默认模式)下,所有指针都是不透明的。可以使用 LLVM 工具(如 opt 中的 -opaque-pointers=0)或 clang 中的 -Xclang -no-opaque-pointers 禁用不透明指针模式。此外,对于显式提及 i8* 样式类型化指针的 IR 和 bitcode 文件,不透明指针模式会自动禁用。

在不透明指针模式下,IR、bitcode 中使用的所有类型化指针,或使用 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 中间层和后端,这通常通过检查相关操作的类型来完成。例如,内存访问相关的分析和优化应该使用 load 和 store 指令中编码的类型,而不是查询指针类型。

以下是一些避免访问指针元素类型的常用方法

  • 对于 load 指令,使用 getType()

  • 对于 store 指令,使用 getValueOperand()->getType()

  • 使用 getLoadStoreType() 在一次调用中处理上述两种情况。

  • 对于 getelementptr 指令,使用 getSourceElementType()

  • 对于 call 指令,使用 getFunctionType()

  • 对于 alloca 指令,使用 getAllocatedType()

  • 对于全局变量,使用 getValueType()

  • 对于一致性断言,使用 PointerType::isOpaqueOrPointeeTypeEquals()

  • 要在不同的地址空间中创建指针类型,请使用 PointerType::getWithSamePointeeType()

  • 要检查两个指针是否具有相同的元素类型,请使用 PointerType::hasSameElementTypeAs()

  • 虽然最好以接受类型化指针和不透明指针的方式编写代码,但可以使用 Type::isOpaquePointerTy()PointerType::isOpaque() 来专门处理不透明指针。PointerType::getNonOpaquePointerElementType() 可以用作显式排除不透明指针的代码路径中的标记。

  • 要获取 byval 参数的类型,请使用 getParamByValType()。对于其他需要知道元素类型的 ABI 影响属性(例如 byref、sret、inalloca 和 preallocated),存在类似的方法。

  • 某些 intrinsic 需要 elementtype 属性,可以使用 getParamElementType() 检索该属性。在 intrinsic 没有自然编码所需元素类型的情况下,需要此属性。这也用于内联汇编。

请注意,上面提到的一些方法仅用于同时支持类型化指针和不透明指针,并且一旦迁移完成将被删除。例如,一旦所有指针都是不透明的,isOpaqueOrPointeeTypeEquals() 将变得毫无意义。

虽然直接使用指针元素类型在代码中立即显而易见,但不透明指针需要解决一个更微妙的问题:许多代码假设指针相等也意味着使用的 load/store 类型或 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
}

在没有不透明指针的情况下,检查 load 和 store 的指针操作数是否相同也确保了访问的类型相同。使用不同的类型需要 bitcast,这将导致不同的指针操作数。

使用不透明指针,bitcast 不存在,并且此检查不再足够。在上面的示例中,它可能导致错误类型的 store 到 load 的转发。进行此类假设的代码需要进行调整以显式检查访问的类型: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。