调试信息赋值跟踪¶
赋值跟踪是一种替代技术,用于在 LLVM 的优化过程中跟踪变量位置调试信息。它为赋值操作提供准确的变量位置,其中局部变量(或其字段)是 LHS。在极少数和复杂的情况下,间接赋值可能会在未被跟踪的情况下被优化掉,但在其他情况下,我们会尽最大努力跟踪所有变量位置。
核心思想是跟踪更多关于源赋值的信息,并保留足够的信息,以便能够在中间端优化运行之后再决定是使用非内存位置(寄存器、常量)还是内存位置。这与使用 #dbg_declare
和 #dbg_value
相反,后者是尽早为大多数变量做出决定,这可能导致次优的变量位置,这些位置可能是不正确或不完整的。
赋值跟踪的第二个目标是尽量减少 LLVM pass 编写者的额外工作,并尽量减少对 LLVM 的总体干扰。
状态和用法¶
状态:默认在 Clang 中启用,但在某些情况下禁用(可以使用 forced
选项覆盖,见下文)。除非被要求(-passes=declare-to-assign
),否则 opt
不会运行该 pass。
标志:-Xclang -fexperimental-assignment-tracking=<disabled|enabled|forced>
启用后,Clang 会使 LLVM 运行 declare-to-assign
pass。该 pass 将传统的调试记录转换为赋值跟踪元数据,并将模块标志 debug-info-assignment-tracking
设置为值 i1 true
。要检查模块是否启用了赋值跟踪,请调用 isAssignmentTrackingEnabled(const Module &M)
(来自 llvm/IR/DebugInfo.h
)。
设计和实现¶
赋值标记:#dbg_assign
¶
#dbg_value
,一种传统的调试记录,标记了 IR 中变量取特定值的位置。类似地,赋值跟踪使用名为 #dbg_assign
的记录标记赋值的位置。
为了知道在 IR 中的什么位置适合为变量使用内存位置,每个赋值标记必须以某种方式引用执行赋值的 store(如果存在,或多个!)。这样,在做出选择时,可以将 store 和标记的位置一起考虑。引用 store 的另一个重要好处是,我们可以构建 store<->标记的双向映射,用于查找在 store 被修改时需要更新的标记。
未链接到任何指令的 #dbg_assign
标记表示执行赋值的 store 已被优化掉,因此内存位置在程序的至少一部分中将无效。
这是 #dbg_assign
的签名。Value *
类型参数首先包装在 ValueAsMetadata
中
#dbg_assign(Value *Value,
DIExpression *ValueExpression,
DILocalVariable *Variable,
DIAssignID *ID,
Value *Address,
DIExpression *AddressExpression)
前三个参数看起来和行为都像 #dbg_value
。ID
是对 store 的引用(见下一节)。Address
是 store 的目标地址,它被 AddressExpression
修改。空/undef/poison 地址表示地址组件已被 kill(内存地址不再是有效位置)。LLVM 当前在 DIExpression
中编码变量片段信息,因此作为实现上的怪癖,Variable
的 FragmentInfo
仅包含在 ValueExpression
中。
指令链接:DIAssignID
¶
DIAssignID
元数据是当前用于编码 store<->标记链接的机制。元数据节点没有操作数,所有实例都是 distinct
;通过比较地址来检查相等性。
#dbg_assign
记录使用 DIAssignID
元数据节点实例作为操作数。这样,它引用任何具有相同 DIAssignID
附件的类似 store 的指令。例如,对于这个 test.cpp,
int fun(int a) {
return a;
}
在没有优化的情况下编译
$ clang++ test.cpp -o test.ll -emit-llvm -S -g -O0 -Xclang -fexperimental-assignment-tracking=enabled
我们得到
define dso_local noundef i32 @_Z3funi(i32 noundef %a) #0 !dbg !8 {
entry:
%a.addr = alloca i32, align 4, !DIAssignID !13
#dbg_assign(i1 undef, !14, !DIExpression(), !13, i32* %a.addr, !DIExpression(), !15)
store i32 %a, i32* %a.addr, align 4, !DIAssignID !16
#dbg_assign(i32 %a, !14, !DIExpression(), !16, i32* %a.addr, !DIExpression(), !15)
%0 = load i32, i32* %a.addr, align 4, !dbg !17
ret i32 %0, !dbg !18
}
...
!13 = distinct !DIAssignID()
!14 = !DILocalVariable(name: "a", ...)
...
!16 = distinct !DIAssignID()
第一个 #dbg_assign
通过 !DIAssignID !13
引用 alloca
,第二个通过 !DIAssignID !16
引用 store
。
类似 store 的指令¶
在缺少链接的 #dbg_assign
的情况下,对已知是变量后备存储的地址的 store 被认为表示对该变量的赋值。
这为我们提供了一个安全的回退,以应对 #dbg_assign
记录已被删除、store 上的 DIAssignID
附件已被删除或优化器已将曾经是间接的 store(未通过赋值跟踪跟踪)变为直接 store 的情况。
中间端:pass 编写者的注意事项¶
非调试指令更新¶
克隆 指令:无需做任何新的事情。克隆会自动克隆 DIAssignID
附件。多个指令可能具有相同的 DIAssignID
指令。在这种情况下,赋值被认为发生在程序中的多个位置。
移动 非调试指令:无需做任何新的事情。链接到 #dbg_assign
的指令的初始 IR 位置由 #dbg_assign
的位置标记。
删除 非调试指令:无需做任何新的事情。简单的 DSE 不需要任何更改;删除带有 DIAssignID
附件的指令是安全的。#dbg_assign
使用未附加到任何指令的 DIAssignID
表示内存位置无效。
合并 store:在许多情况下,不需要更改,因为如果调用 combineMetadata
,DIAssignID
附件会自动合并。无论如何,都必须合并 DIAssignID
附件,以便新的 store 与合并的 store 链接的所有 #dbg_assign
记录链接。这可以通过简单地调用辅助函数 Instruction::mergeDIAssignID
来实现。
内联 store:当 store 被内联时,我们生成 #dbg_assign
记录和 DIAssignID
附件,就好像 store 表示源赋值一样,就像在前端一样。这并不完美,因为 store 可能在内联之前已被移动、修改或删除,但它至少保持了变量信息在非内联作用域内的正确性。
拆分 store:SROA 和拆分 store 的 pass 将 #dbg_assign
记录与 #dbg_declare
记录类似地处理。克隆链接到 store 的 #dbg_assign
记录,更新 ValueExpression
中的 FragmentInfo,并为拆分的 store(和克隆的记录)分别赋予新的 DIAssignID
附件。换句话说,将拆分的 store 视为单独的赋值。对于部分 DSE(例如,缩短 memset),我们执行相同的操作,不同之处在于死片段的 #dbg_assign
获取一个 Undef
Address
。
提升 allocas 和 store/loads:#dbg_assign
记录隐式描述了 CFG 连接处内存位置中连接的值,但这在提升(或部分提升)变量后不一定如此。提升变量的 pass 负责在提升期间生成的 resultant PHI 之后插入 #dbg_assign
记录。mem2reg
已经必须为 #dbg_declare
s 执行此操作(使用 #dbg_value
)。如果 store 没有链接的记录,则假定该 store 表示对存储在目标地址的变量的赋值。
调试记录更新¶
移动 调试记录:尽可能避免移动 #dbg_assign
记录,因为它们表示源级赋值,其在程序中的位置不应受优化 pass 的影响。
删除 调试记录:无需做任何新的事情。与传统的调试记录一样,除非它是不可达的,否则几乎总是错误地删除 #dbg_assign
记录。
将 #dbg_assign
降低到 MIR¶
首先,仅支持 SelectionDAG ISel。#dbg_assign
记录被降低为 MIR DBG_INSTR_REF
指令。在此之前,我们需要决定在何处适合使用内存位置,以及在何处必须为每个变量使用非内存位置(或不使用位置)。为了做出这些决定,我们运行一个标准的定点数据流分析,该分析在每个指令处做出选择,迭代地连接每个块的结果。
TODO 列表¶
待改进项
如 llvm/test/DebugInfo/assignment-tracking/X86/diamond-3.ll 测试中所述,分析应将转义调用视为未标记的 store。
该系统期望局部变量由本地 alloca 支持。情况并非总是如此 - 有时指向存储的指针会传递到函数中(例如 sret、byval)。我们需要能够处理这些情况。有关示例,请参见 llvm/test/DebugInfo/Generic/assignment-tracking/track-assignments.ll 和 clang/test/CodeGen/assignment-tracking/assignment-tracking.cpp。
trackAssignments
尚不适用于其#dbg_declare
位置被DIExpression
修改的变量,例如,当变量的地址本身存储在使用#dbg_declare
和DIExpression(DW_OP_deref)
的alloca
中时。有关示例,请参见 llvm/test/DebugInfo/Generic/assignment-tracking/track-assignments.ll 中的indirectReturn
和 clang/test/CodeGen/assignment-tracking/assignment-tracking.cpp。为了解决第一个要点,我们需要能够指定内存位置可用而无需使用
DIAssignID
。这是因为存储地址不是由指令计算的(它是一个参数值),因此我们无处放置元数据附件。为了解决这个问题,我们可能需要另一个标记记录来表示“变量的堆栈家是 X 地址” - 类似于#dbg_declare
,但它需要与#dbg_assign
记录组合,以便仅当#dbg_assign
记录同意时,堆栈家地址才被选为变量的位置。鉴于上述情况(特殊的“堆栈家是 X”记录),以及我们只能跟踪具有固定偏移量和大小的赋值的事实,我认为我们可能可以摆脱地址和地址表达式部分,因为它始终可以用我们拥有的信息计算出来。
对于 LTO 和 thinLTO 构建,以及如果已指定 LLDB 调试器调优,则默认禁用赋值跟踪。我们应该取消这些限制。请参阅 clang/lib/CodeGen/BackendUtil.cpp 中的 EmitAssemblyHelper::RunOptimizationPipeline。