除錯資訊賦值追蹤¶
賦值追蹤是一種替代技術,用於在 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
修改。空/未定義/poison 地址表示地址組件已被移除(記憶體地址不再是有效位置)。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_declare
記錄的方式處理 #dbg_assign
記錄。複製連結到 store 指令的 #dbg_assign
記錄,更新 ValueExpression
中的 FragmentInfo,並為分割的 store 指令(和複製的記錄)分別提供新的 DIAssignID
附件。換句話說,將分割的 store 指令視為單獨的賦值。對於部分 DSE(例如,縮短 memset),我們執行相同的操作,除了死片段的 #dbg_assign
獲得 Undef
Address
。
提升 allocas 和 store/loads:#dbg_assign
記錄隱式地描述了 CFG 連接點處記憶體位置中連接的值,但在提升(或部分提升)變數後,情況不一定如此。提升變數的 Pass 負責在提升期間產生的結果 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 列表¶
未完成的改進
如 test llvm/test/DebugInfo/assignment-tracking/X86/diamond-3.ll 中所述,分析應將 escape 呼叫視為未標記的 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
修改的變數,例如,當變數的地址本身儲存在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」記錄),以及我們只能追蹤具有固定偏移量和大小的賦值,我認為我們可能可以擺脫地址和地址表示式部分,因為它始終可以使用我們擁有的資訊計算出來。
預設情況下,對於 LTO 和 thinLTO 建置,以及如果已指定 LLDB 除錯器調整,則會停用賦值追蹤。我們應該刪除這些限制。請參閱 clang/lib/CodeGen/BackendUtil.cpp 中的 EmitAssemblyHelper::RunOptimizationPipeline。