不透明指针

不透明指针类型

传统上,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 的开发,而不是帮助它。一些最初提出的高级优化已经发展成为 TBAA,这是由于通过 SSA 值直接表示高级语言信息存在限制。

被指向类型为前端提供了一些价值,因为 IR 验证器使用类型来检测简单的类型混淆错误。但是,前端也必须处理在可能需要的所有地方插入位转换的复杂性。社区共识是,被指向类型的成本超过了收益,并且应该将其移除。

许多操作实际上并不关心底层类型。这些操作,通常是内联函数,通常最终会采用任意指针类型 i8*,有时还会采用大小。这导致 IR 中出现大量冗余的无操作位转换,以在具有不同被指向类型的指针之间转换。

无操作位转换占用内存/磁盘空间,并且还会占用编译时间进行查看。但是,也许最大的问题是处理位转换所需的代码复杂性。在查看指针的 def-use 链时,很容易忘记调用 Value::stripPointerCasts() 以查找被位转换隐藏的真正底层指针。当向下查看 def-use 链时,传递需要迭代位转换以处理使用情况。移除无操作指针位转换可以防止一类错失的优化,并使编写 LLVM 传递变得稍微容易一些。

更少的无操作指针位转换也减少了地址空间方面不正确位转换的可能性。维护非常关心地址空间的后端的人员抱怨说,像 Clang 这样的前端经常错误地转换指针,从而丢失地址空间信息。

LLVM 中较早发生的类似转换是整数有符号性。目前,整数类型之间没有有符号和无符号的区别,而是每个整数操作(例如加法)都包含标志以指示如何处理整数。以前,LLVM IR 区分无符号和有符号整数类型,并遇到了类似的无操作转换问题。将有符号性从类型转换为指令的转换在 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 中间端和后端中,这通常是通过检查相关操作的类型来实现的。例如,与内存访问相关的分析和优化应该使用加载和存储指令中编码的类型,而不是查询指针类型。

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

  • 对于加载,使用 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 和预分配。

  • 一些内联函数需要 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。