调试信息赋值跟踪

赋值跟踪是一种替代技术,用于在 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_valueID 是对 store 的引用(见下一节)。Address 是 store 的目标地址,它被 AddressExpression 修改。空/undef/poison 地址表示地址组件已被 kill(内存地址不再是有效位置)。LLVM 当前在 DIExpression 中编码变量片段信息,因此作为实现上的怪癖,VariableFragmentInfo 仅包含在 ValueExpression 中。

类似 store 的指令

在缺少链接的 #dbg_assign 的情况下,对已知是变量后备存储的地址的 store 被认为表示对该变量的赋值。

这为我们提供了一个安全的回退,以应对 #dbg_assign 记录已被删除、store 上的 DIAssignID 附件已被删除或优化器已将曾经是间接的 store(未通过赋值跟踪跟踪)变为直接 store 的情况。

中间端:pass 编写者的注意事项

非调试指令更新

克隆 指令:无需做任何新的事情。克隆会自动克隆 DIAssignID 附件。多个指令可能具有相同的 DIAssignID 指令。在这种情况下,赋值被认为发生在程序中的多个位置。

移动 非调试指令:无需做任何新的事情。链接到 #dbg_assign 的指令的初始 IR 位置由 #dbg_assign 的位置标记。

删除 非调试指令:无需做任何新的事情。简单的 DSE 不需要任何更改;删除带有 DIAssignID 附件的指令是安全的。#dbg_assign 使用未附加到任何指令的 DIAssignID 表示内存位置无效。

合并 store:在许多情况下,不需要更改,因为如果调用 combineMetadataDIAssignID 附件会自动合并。无论如何,都必须合并 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_declares 执行此操作(使用 #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_declareDIExpression(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。