调试信息赋值跟踪¶
赋值跟踪是一种替代技术,用于在 LLVM 的优化过程中跟踪变量位置调试信息。它为局部变量(或其字段)作为左值的情况提供准确的变量位置。在一些罕见且复杂的情况下,间接赋值可能会在未被跟踪的情况下被优化掉,但在其他情况下,我们会尽最大努力跟踪所有变量位置。
其核心思想是按顺序跟踪有关源代码赋值的更多信息,并保留足够的信息,以便能够推迟关于是否使用非内存位置(寄存器、常量)或内存位置的决策,直到中间端优化运行完毕。这与使用#dbg_declare
和 #dbg_value
形成对比,后者是在早期为大多数变量做出决策,这可能导致次优的变量位置,这些位置可能不正确或不完整。
赋值跟踪的次要目标是为 LLVM 传递编写者带来最少的额外工作,并尽量减少对 LLVM 本身的干扰。
状态和用法¶
状态:正在进行的实验性工作。除非用于开发和测试,否则强烈建议不要启用。
在 Clang 中启用:-Xclang -fexperimental-assignment-tracking
这会导致 Clang 让 LLVM 运行 declare-to-assign
传递。该传递将传统的调试记录转换为赋值跟踪元数据,并将模块标志 debug-info-assignment-tracking
设置为值 i1 true
。要检查模块是否启用了赋值跟踪,请调用 isAssignmentTrackingEnabled(const Module &M)
(来自 llvm/IR/DebugInfo.h
)。
设计与实现¶
赋值标记:#dbg_assign
¶
#dbg_value
,一种传统的调试记录,标记出 IR 中变量取特定值的位置。类似地,赋值跟踪使用名为 #dbg_assign
的记录来标记赋值的位置。
为了知道在 IR 的哪个位置适合为变量使用内存位置,每个赋值标记必须以某种方式引用执行赋值的存储(如果有的话,或者多个!)。这样,在做出该选择时,就可以将存储和标记的位置一起考虑。引用存储的另一个重要好处是,我们可以构建存储<->标记的双向映射,该映射可用于查找在存储被修改时需要更新的标记。
未链接到任何指令的 #dbg_assign
标记表示执行赋值的存储已被优化掉,因此内存位置对于程序的至少一部分将无效。
以下是 #dbg_assign
签名。Value *
类型参数首先包装在 ValueAsMetadata
中
#dbg_assign(Value *Value,
DIExpression *ValueExpression,
DILocalVariable *Variable,
DIAssignID *ID,
Value *Address,
DIExpression *AddressExpression)
前三个参数看起来并表现得像 #dbg_value
。 ID
是对存储的引用(参见下一节)。Address
是存储的目标地址,它由 AddressExpression
修改。空/未定义/中毒地址表示地址组件已被删除(内存地址不再是有效位置)。LLVM 目前在 DIExpression
中编码变量片段信息,因此作为实现上的怪癖,Variable
的 FragmentInfo
仅包含在 ValueExpression
中。
指令链接:DIAssignID
¶
DIAssignID
元数据是当前用于编码存储<->标记链接的机制。元数据节点没有操作数,所有实例都是 distinct
;通过比较地址来检查相等性。
#dbg_assign
记录使用 DIAssignID
元数据节点实例作为操作数。这样,它就可以引用任何具有相同 DIAssignID
附加的类似存储的指令。例如,对于此 test.cpp,
int fun(int a) {
return a;
}
在不进行优化的情况下编译
$ clang++ test.cpp -o test.ll -emit-llvm -S -g -O0 -Xclang -fexperimental-assignment-tracking
我们得到
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
。
类似存储的指令¶
在没有链接的 #dbg_assign
的情况下,对已知是变量的后备存储的地址的存储被认为表示对该变量的赋值。
这为我们提供了一个安全的后备方案,在 #dbg_assign
记录已被删除、存储上的 DIAssignID
附加已被删除或优化器已将曾经间接的存储(未用赋值跟踪进行跟踪)变为直接存储的情况下。
中间端:传递编写者的注意事项¶
非调试指令更新¶
克隆指令:无需执行任何新操作。克隆会自动克隆 DIAssignID
附加。多个指令可能具有相同的 DIAssignID
指令。在这种情况下,赋值被认为发生在程序中的多个位置。
移动非调试指令:无需执行任何新操作。链接到 #dbg_assign
的指令的初始 IR 位置由 #dbg_assign
的位置标记。
删除非调试指令:无需执行任何新操作。简单的 DSE 不需要任何更改;删除具有 DIAssignID
附加的指令是安全的。使用未附加到任何指令的 DIAssignID
的 #dbg_assign
指示内存位置无效。
合并存储:在许多情况下,不需要更改,因为如果调用了 combineMetadata
,DIAssignID
附加会自动合并。无论如何,必须合并 DIAssignID
附加,以便新的存储链接到合并的存储链接到的所有 #dbg_assign
记录。这可以通过简单地调用辅助函数 Instruction::mergeDIAssignID
来实现。
内联存储:随着存储被内联,我们生成 #dbg_assign
记录和 DIAssignID
附加,就像存储表示源代码赋值一样,就像在前端一样。这并不完美,因为存储可能在内联之前已被移动、修改或删除,但它至少使非内联范围内的变量信息保持正确。
拆分存储:SROA 和拆分存储的传递将 #dbg_assign
记录视为与 #dbg_declare
记录类似。克隆链接到存储的 #dbg_assign
记录,更新 ValueExpression
中的 FragmentInfo
,并分别为拆分的存储(和克隆的记录)提供新的 DIAssignID
附加。换句话说,将拆分的存储视为单独的赋值。对于部分 DSE(例如,缩短 memset),我们执行相同的操作,只是已死片段的 #dbg_assign
获取 Undef
Address
。
提升 alloca 和存储/加载:#dbg_assign
记录隐式地描述了 CFG 连接处内存位置中的连接值,但在提升(或部分提升)变量后,情况并非总是如此。提升变量的传递负责在提升期间生成的生成的 PHI 之后插入 #dbg_assign
记录。mem2reg
已经必须为此(使用 #dbg_value
)执行 #dbg_declare
操作。在存储没有链接记录的情况下,该存储被假定为表示存储在目标地址处的变量的赋值。
调试记录更新¶
移动调试记录:尽可能避免移动 #dbg_assign
记录,因为它们表示源代码级赋值,其在程序中的位置不应受优化传递的影响。
删除调试记录:无需做任何新的操作。与传统的调试记录一样,除非它不可达,否则删除#dbg_assign
记录几乎总是错误的。
将#dbg_assign
降低到MIR¶
首先,只支持SelectionDAG ISel。#dbg_assign
记录被降低到MIR DBG_INSTR_REF
指令。在此之前,我们需要确定何时使用内存位置,以及何时必须为每个变量使用非内存位置(或无位置)。为了做出这些决定,我们运行一个标准的固定点数据流分析,该分析在每个指令处做出选择,并迭代地合并每个块的结果。
TODO 列表¶
由于这是一项正在进行的实验性工作,因此我们还有一些项目需要处理。
如 llvm/test/DebugInfo/assignment-tracking/X86/diamond-3.ll 中所述,分析应该将逃逸调用视为未标记的存储。
系统期望局部变量由局部 alloca 支持。情况并非总是如此 - 有时会将指向存储区的指针传递给函数(例如 sret、byval)。我们需要能够处理这些情况。有关示例,请参见 llvm/test/DebugInfo/Generic/assignment-tracking/track-assignments.ll 和 clang/test/CodeGen/assignment-tracking/assignment-tracking.cpp。
trackAssignments
尚未适用于其#dbg_declare
位置被DIExpression
修改的变量,例如,当变量的地址本身存储在alloca
中时,#dbg_declare
使用DIExpression(DW_OP_deref)
。有关示例,请参见 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”记录),以及我们只能跟踪具有固定偏移量和大小的赋值的事实,我认为我们可能可以删除地址和地址表达式部分,因为它总是可以用我们拥有的信息计算出来。