LLVM IR 中的可扩展元数据
前端作者的一个常见需求是能够向 LLVM IR 添加某种元数据。这种元数据可以用来影响特定语言的优化过程(例如,C 中的基于类型的别名分析)、为自定义代码生成器标记信息或将信息传递到链接时优化。LLVM 2.7 为此提供了首要的支持,并且已将调试信息切换到使用它(改进调试信息!)。虽然可以在 LLVM 语言参考 手册中找到此功能的详细信息,但有时很难从低级细节中提炼出全局图景。这篇文章试图通过解释 LLVM 2.7 此新功能的一些历史、动机和示例用例来填补这一空白。
这篇文章由 Devang Patel 和我共同撰写。
在我们深入探讨元数据的工作原理之前,了解 LLVM 2.6 及更早版本中如何表示调试信息非常有用。
LLVM 2.6 中的调试信息
调试信息将源位置信息、类型信息和变量信息传达给调试器。此信息在程序执行期间不会使用,也不会在目标文件中产生可执行代码,但代码生成器会使用它来生成 DWARF 信息。这样,调试信息就成为一种从前端到代码生成器中 DWARF 发射器的一种侧通道。
由于缺乏更好的机制,在 LLVM 2.6 及更早版本中,调试信息使用标记有特殊 "llvm.metadata" 部分的全局变量进行编码。例如,我们将生成类似这样的内容来描述类似 "int my_data;" 的 C 代码。
@my_data = common global i32 0, align 4
@llvm.dbg.global_variable = internal constant %llvm.dbg.global_variable.type {
i32 458804,
{}* bitcast (%llvm.dbg.anchor.type* @llvm.dbg.global_variables to { }*),
{}* bitcast (%llvm.dbg.compile_unit.type* @llvm.dbg.compile_unit to { }*),
i8* getelementptr ([8 x i8]* @.str4, i32 0, i32 0),
i8* getelementptr ([8 x i8]* @.str4, i32 0, i32 0),
i8* null,
{}* bitcast (%llvm.dbg.compile_unit.type* @llvm.dbg.compile_unit to { }*),
i32 2,
{}* bitcast (%llvm.dbg.basictype.type* @llvm.dbg.basictype to { }*),
i1 false,
i1 true,
{}* bitcast (i32* @my_data to {}*)
}, section "llvm.metadata"
@.str4 = internal constant [8 x i8] c"my_data\00", section "llvm.metadata"
在此示例中,@my_data 是为 C 变量生成的实际全局变量。无论是否启用调试信息,都会生成它。
在这里,@.str4 和 @llvm.dbg.global_variable 被代码生成器解释为描述符,它们包含有关 @my_data 的信息,其中包含各种字段,指示声明全局变量的行号、编译单元等。您可以在 LLVM2.6 调试信息文档 中看到这些字段的完整描述。在代码生成时,矮人编写器将遍历此信息并将其转换为 DWARF 信息。LLVM 全局变量将不会像普通代码那样发出,因为它们位于魔法 llvm.metadata 部分中。
虽然这确实提供了一定程度的基本功能,但它也存在一些缺点。首先,@my_data 全局变量在 IR 中有一个额外的用途,这可能会影响对 @my_data 的优化。例如,如果已死,则死全局变量消除过程不会将其删除,并且 mod/ref 分析过程不会对其进行分析,因为它似乎获取了它的地址。调试信息的主要目标是启用它不应影响编译器生成的执行代码。如果它确实影响了,则启用调试信息可能会隐藏您正在尝试追踪的错误!
此实现的第二个缺点是它包含大量毫无意义的位转换常量表达式。这些额外的对象会膨胀内存占用空间,需要时间进行分配、唯一化和优化等。位转换也会对 LLVM 中间代码的可读性产生负面影响,并且完全没有必要:矮人发射器并不关心类型,它正在遍历此信息作为数据结构,而不是将其发射到内存中。
LLVM IR 元数据的动机
基于我们在调试信息方面的经验,以及实施新酷功能的愿望,我们 设计 并实施了一个勇敢的新世界,其中元数据实际上是 LLVM IR 的一等公民。该设计旨在解决上面提到的问题。
- 优化不应受到元数据的影響,除非它們明确地尝试查看它。
- 我们希望减少调试信息的内存占用空间和成本。
- 元数据不应具有 LLVM IR 类型。
- 理想情况下,应该减少语法上的混乱,提高人们解码这些内容的几率。
另一个重要的设计点是,我们希望能够添加新的元数据形式,而无需更新优化器来支持它们。这是一个关键的设计点,因为我们希望元数据可以由前端作者扩展来执行任何他们想要的操作,并且不应该需要修改优化器。
LLVM 2.7 中的元数据
元数据支持包含几个不同的相关 IR 扩展:LLVM IR 中的新 'metadata' 类型、新的 MDString、MDNode 和 NamedMDNode 类(所有这三个类都派生自 'Value')、增加了对从 内在函数 中引用元数据的支持,以及对 将其附加到指令 的支持。元数据支持通常位于 llvm/Metadata.h 标头中。我们将依次介绍这些构造。
新的 'metadata' 类型 是每个新 IR 对象的 LLVM 类型。这确保您不能将元数据用作随机指令的操作数,例如,您不能执行 'add i32 4, !"str"',因为元数据不是一等公民类型。对元数据的限制意味着它只能作为内在函数的参数、作为另一个元数据的操作数、在模块中的顶层(NamedMDNode)或附加到指令中。
新的 MDString 类 用于在元数据中表示字符串数据,并且它始终具有元数据类型。由于 MDString 旨在用作元数据而不是代码,因此它们在 .ll 文件中没有以 null 结尾。MDString 类允许遍历 IR 的 C++ 代码使用 StringRef 访问任意字符串数据。在 .ll 文件中,它的语法类似于
!"foo"
新的 MDNode 类 是一个元组,它可以引用程序中的任意 LLVM IR 值以及其他元数据。在 .ll 文件中,MDNode 会被编号,引用一个 MDNode 的语法是 "!123",其中 123 是被引用的节点的编号。使用类似以下内容声明 MDNode。
!23 = !{ i32 4, !"foo", i32 *@G, metadata !22 }
在这种情况下,MDNode 有四个操作数,第一个是 ConstantInt,第二个是 MDString,第三个是全局变量,第四个是另一个 MDNode。MDNode 有两种类型:一种是普通的全局 MDNode,它引用全局变量、常量等。第二种是函数局部 MDNode,它可以(可能间接地)引用特定函数内的指令。MDNode 的一个重要方面是它们不被视为值的 "使用":例如,它们不会被 use_iterator 找到,并且不会被计入 Value::hasOneUse() 等谓词中。这可以防止元数据意外地影响代码生成。
新的 NamedMDNode 类 提供了对模块级别元数据的命名访问,并且每个 NamedMDNode 都包含一个 MDNode 列表。这使元数据的客户端(例如调试信息)能够找到所有特定形式的元数据(例如全局变量调试描述符)。Module 类维护 NamedMDNode 实例列表,就像它维护全局变量、函数和别名一样。在 .ll 文件中,NamedMDNode 如下所示
!my_named_mdnode = !{ !1, !2, !4212 }
这定义了一个具有三个引用 MDNode 的 NamedMDNode。
LLVM 内在函数可以引用元数据 作为普通操作数。更确切地说,它们可以直接引用 MDNode 和 MDString 对象,即使其他调用和其他操作不能引用。在 .ll 文件中,这看起来类似于
!0 = metadata !{i32 524544, ...
...
%x = alloca i32
call void @llvm.dbg.declare(metadata !{i32* %x}, metadata !0)
这将模块级 !0 MDNode 传递到第二个参数,并将函数局部 MDNode 作为第一个参数传递(由于它是一个 mdnode,因此不计为 %X 的使用)。在这种情况下,代码生成器使用此信息来了解元数据 !0 是 alloca %X 的变量描述符。请注意,内在函数本身不被视为元数据,因此它们会影响代码生成等。
最后,元数据可以附加到指令。指令可以具有任意数量的 MDNode 附加到它们,并带有字符串标签。例如
store i32 0, i32* %P, !nontemporal !2, !frobnatz !123
ret void, !dbg !9
第一个示例是一个带有两个指令级元数据记录附加到它的存储,一个名为 'nontemporal'(已 在 LLVM 2.7 中实现)和一个名为 'frobnatz'(这是一个可能在 LLVM 2.8 中出现的新功能)。第二个示例是一个带有调试位置附加到它的返回指令。
将可扩展元数据用于调试信息
为了与上面的 LLVM 2.6 调试信息示例进行对比,在 LLVM 2.7 中,我们会得到类似这样的内容
@my_data = common global i32 0, align 4
!llvm.dbg.gv = !{!0}
!0 = metadata !{
i32 524340, i32 0, metadata !1, metadata !"my_data", metadata !"my_data",
metadata !"", metadata !1, i32 2, metadata !3,
i1 false, i1 true, i32* @my_data
}
这将全局变量替换为 MDNode 和 MDString。这通过消除毫无意义的位转换、消除无关的 IR 类型以及 !0 对 @my_data 的使用不再被视为 "使用" 来缩小 IR。但是,我们仍然有大量 在其他地方记录的魔法字段。
如果您想查看更多调试信息的示例,可以使用类似以下内容来查看前端生成的调试信息clang foo.c -g -S -o - -emit-llvm | less".
元数据的用途
上面提到过一个微妙的点,即我们不希望优化器必须了解元数据。虽然让优化器保留特定元数据是完全可行的(例如,循环强度降低可以做一些花哨的事情来更新调试信息,如果需要的话),但默认情况下,优化器会忽略和破坏它。例如,如果优化器删除了指令,并且有一个函数级 MDNode 引用它,则 MDNode 中的引用会隐式地降级为 null。
这对元数据的安全用途有一些重要的影响:它只能用于 "增值" 信息,即不会更改程序语义的信息。为了重复这个重要点,只有在程序在元数据被静默删除时保留其语义的情况下,使用元数据才安全。
例如,对于调试信息来说,使用元数据是微不足道的安全操作(尽管矮人发射器必须小心以容忍 null 指针!):如果元数据被删除,它只是意味着调试信息的质量会降低,它不会使调试信息本身无效。在上面的示例中,如果全局 "my_data" 被优化器删除,则引用将降级为 null,调试信息发射器将不会为 my_data 生成位置。
虽然这听起来可能很有限,但元数据有很多潜在的用例,您只需要小心如何构建它。让我们来看几个示例。
元数据的当前和潜在客户端
LLVM 2.7 支持使用 `!nontemporal` 指令级修饰符生成**非时间性加载和存储**,如 LangRef 手册中所述。非时间性访问是一种正常的访问,但提示 CPU 可以避免将数据拉入缓存,因为它近期不会再次访问。这是安全的,因为 `!nontemporal` 是一个优化提示:删除 `!nontemporal` 提示将导致优化器生成正常的加载和存储,这可能会导致性能下降,但提供与实际非时间性访问相同的语义。
一个潜在的未来用例是支持**类型基础别名分析 (TBAA)**。TBAA 是一种优化,用于确定 "float *P1" 和 "int *P2" 永不别名(在 GCC 中,这可以通过 `-fstrict-aliasing` 启用)。这个技巧在于,用 LLVM IR 类型来实现 TBAA 不安全,你实际上需要能够根据复杂的源代码级规则(例如,在 C 中,"char*" 可以别名任何东西)来编码和表达类型子集图。
LLVM 中的 TBAA 实现将使用 MDNodes 编码类型子集图,并使用 `!tbaa` 指令标记将类型标记添加到加载和存储操作中。一个新的别名分析实现将查找访问中的这些标记,并遍历类型子集图,以确定两个访问是否可能彼此别名。这种元数据的使用也是安全的,因为它是一种优化:如果类型标记被删除,则始终可以安全地假设访问对于 TBAA 目的来说别名于所有内容。
更广泛地说,元数据是**前端向自定义语言特定优化传递传递任意信息**的绝佳方式。TBAA 是一个例子,但这同样适用于诸如反虚拟化(通过类层次结构分析)、进行锁和异常处理优化,甚至以库为中心的优化都可以通过这种方式实现。
由于 LLVM 2.7 只是第一个支持其 IR 中元数据的版本,我们还没有看到它最终将如何使用。如果你在使用它的过程中发现了新颖或有趣的方法,请向我发送一个链接,描述你的使用方法,我将从这篇文章链接到它。
-Chris 和 Devang