使用 XRay 除錯¶
本文件展示了如何分析使用 XRay 檢測構建的應用程式的範例。在這裡,我們將嘗試除錯 llc
編譯由 Clang 生成的一些範例 LLVM IR。
使用 XRay 構建¶
要使用 XRay 檢測除錯應用程式,我們需要使用支援 -fxray-instrument
選項的 Clang 構建它。如需有關 XRay 工作原理的更多技術細節,請參閱 XRay 以取得背景資訊。
在我們的範例中,我們需要將 -fxray-instrument
新增到構建二進位檔時傳遞給 Clang 的旗標列表中。請注意,我們也需要與 Clang 連結才能正確連結 XRay 執行階段。為了使用 XRay 構建 llc
,我們對 LLVM 構建執行以下類似操作
$ mkdir -p llvm-build && cd llvm-build
# Assume that the LLVM sources are at ../llvm
$ cmake -GNinja ../llvm -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_FLAGS_RELEASE="-fxray-instrument" -DCMAKE_CXX_FLAGS="-fxray-instrument" \
# Once this finishes, we should build llc
$ ninja llc
為了驗證我們是否有一個 XRay 檢測過的二進位檔,我們可以使用 objdump
來尋找 xray_instr_map
區段。
$ objdump -h -j xray_instr_map ./bin/llc
./bin/llc: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
14 xray_instr_map 00002fc0 00000000041516c6 00000000041516c6 03d516c6 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
取得追蹤¶
根據預設,XRay 不會寫入追蹤檔案或在主程式開始之前修補應用程式。如果我們執行 llc
,它應該像一個正常構建的二進位檔一樣工作。如果我們想要取得應用程式操作的完整追蹤(我們最終使用 XRay 檢測的函數),那麼我們需要在應用程式啟動時啟用 XRay。為此,XRay 會檢查 XRAY_OPTIONS
環境變數。
# The following doesn't create an XRay trace by default.
$ ./bin/llc input.ll
# We need to set the XRAY_OPTIONS to enable some features.
$ XRAY_OPTIONS="patch_premain=true xray_mode=xray-basic verbosity=1" ./bin/llc input.ll
==69819==XRay: Log file in 'xray-log.llc.m35qPB'
至此,我們現在有了一個可以開始分析的 XRay 追蹤。
llvm-xray
工具¶
擁有追蹤後,我們就可以對已檢測的函數進行基本統計,以及我們在程式碼各個部分花費了多少時間。為了理解這些資料,我們使用 llvm-xray
工具,它有一些子命令可以幫助我們理解我們的追蹤。
我們可以做的一件事是獲取已檢測函數的統計信息。我們可以使用 llvm-xray account
查看示例統計信息
$ llvm-xray account xray-log.llc.m35qPB --top=10 --sort=sum --sortorder=dsc --instr_map=./bin/llc
Functions with latencies: 29
funcid count [ min, med, 90p, 99p, max] sum function
187 360 [ 0.000000, 0.000001, 0.000014, 0.000032, 0.000075] 0.001596 LLLexer.cpp:446:0: llvm::LLLexer::LexIdentifier()
85 130 [ 0.000000, 0.000000, 0.000018, 0.000023, 0.000156] 0.000799 X86ISelDAGToDAG.cpp:1984:0: (anonymous namespace)::X86DAGToDAGISel::Select(llvm::SDNode*)
138 130 [ 0.000000, 0.000000, 0.000017, 0.000155, 0.000155] 0.000774 SelectionDAGISel.cpp:2963:0: llvm::SelectionDAGISel::SelectCodeCommon(llvm::SDNode*, unsigned char const*, unsigned int)
188 103 [ 0.000000, 0.000000, 0.000003, 0.000123, 0.000214] 0.000737 LLParser.cpp:2692:0: llvm::LLParser::ParseValID(llvm::ValID&, llvm::LLParser::PerFunctionState*)
88 1 [ 0.000562, 0.000562, 0.000562, 0.000562, 0.000562] 0.000562 X86ISelLowering.cpp:83:0: llvm::X86TargetLowering::X86TargetLowering(llvm::X86TargetMachine const&, llvm::X86Subtarget const&)
125 102 [ 0.000001, 0.000003, 0.000010, 0.000017, 0.000049] 0.000471 Verifier.cpp:3714:0: (anonymous namespace)::Verifier::visitInstruction(llvm::Instruction&)
90 8 [ 0.000023, 0.000035, 0.000106, 0.000106, 0.000106] 0.000342 X86ISelLowering.cpp:3363:0: llvm::X86TargetLowering::LowerCall(llvm::TargetLowering::CallLoweringInfo&, llvm::SmallVectorImpl<llvm::SDValue>&) const
124 32 [ 0.000003, 0.000007, 0.000016, 0.000041, 0.000041] 0.000310 Verifier.cpp:1967:0: (anonymous namespace)::Verifier::visitFunction(llvm::Function const&)
123 1 [ 0.000302, 0.000302, 0.000302, 0.000302, 0.000302] 0.000302 LLVMContextImpl.cpp:54:0: llvm::LLVMContextImpl::~LLVMContextImpl()
139 46 [ 0.000000, 0.000002, 0.000006, 0.000008, 0.000019] 0.000138 TargetLowering.cpp:506:0: llvm::TargetLowering::SimplifyDemandedBits(llvm::SDValue, llvm::APInt const&, llvm::APInt&, llvm::APInt&, llvm::TargetLowering::TargetLoweringOpt&, unsigned int, bool) const
這顯示了對於我們的輸入文件,llc
在詞法分析器中花費的累積時間最多(總共 1 毫秒)。例如,如果我們想在試算表中使用這些數據,我們可以使用 -format=csv
選項將結果輸出為 CSV 格式,以便進一步分析。
如果我們想獲得原始追蹤的文字表示形式,我們可以使用 llvm-xray convert
工具來獲得 YAML 輸出。示例追蹤的前幾行如下所示
$ llvm-xray convert -f yaml --symbolize --instr_map=./bin/llc xray-log.llc.m35qPB
---
header:
version: 1
type: 0
constant-tsc: true
nonstop-tsc: true
cycle-frequency: 2601000000
records:
- { type: 0, func-id: 110, function: __cxx_global_var_init.8, cpu: 37, thread: 69819, kind: function-enter, tsc: 5434426023268520 }
- { type: 0, func-id: 110, function: __cxx_global_var_init.8, cpu: 37, thread: 69819, kind: function-exit, tsc: 5434426023523052 }
- { type: 0, func-id: 164, function: __cxx_global_var_init, cpu: 37, thread: 69819, kind: function-enter, tsc: 5434426029925386 }
- { type: 0, func-id: 164, function: __cxx_global_var_init, cpu: 37, thread: 69819, kind: function-exit, tsc: 5434426030031128 }
- { type: 0, func-id: 142, function: '(anonymous namespace)::CommandLineParser::ParseCommandLineOptions(int, char const* const*, llvm::StringRef, llvm::raw_ostream*)', cpu: 37, thread: 69819, kind: function-enter, tsc: 5434426046951388 }
- { type: 0, func-id: 142, function: '(anonymous namespace)::CommandLineParser::ParseCommandLineOptions(int, char const* const*, llvm::StringRef, llvm::raw_ostream*)', cpu: 37, thread: 69819, kind: function-exit, tsc: 5434426047282020 }
- { type: 0, func-id: 187, function: 'llvm::LLLexer::LexIdentifier()', cpu: 37, thread: 69819, kind: function-enter, tsc: 5434426047857332 }
- { type: 0, func-id: 187, function: 'llvm::LLLexer::LexIdentifier()', cpu: 37, thread: 69819, kind: function-exit, tsc: 5434426047984152 }
- { type: 0, func-id: 187, function: 'llvm::LLLexer::LexIdentifier()', cpu: 37, thread: 69819, kind: function-enter, tsc: 5434426048036584 }
- { type: 0, func-id: 187, function: 'llvm::LLLexer::LexIdentifier()', cpu: 37, thread: 69819, kind: function-exit, tsc: 5434426048042292 }
- { type: 0, func-id: 187, function: 'llvm::LLLexer::LexIdentifier()', cpu: 37, thread: 69819, kind: function-enter, tsc: 5434426048055056 }
- { type: 0, func-id: 187, function: 'llvm::LLLexer::LexIdentifier()', cpu: 37, thread: 69819, kind: function-exit, tsc: 5434426048067316 }
控制精確度¶
到目前為止,在我們的示例中,我們還沒有完全涵蓋二進制文件中的所有函數。為了實現這一點,我們需要修改編譯器標誌,以便我們可以檢測二進制文件中更多(如果不是全部)的函數。我們有兩種選擇可以做到這一點,我們將在下面探討這兩種選擇。
指令閾值¶
第一種「粗略」的方法是將函數主體的最小閾值設置為 1。我們可以在構建二進制文件時使用 -fxray-instruction-threshold=N
標誌來實現。我們使用此選項重建 llc
並觀察結果
$ rm CMakeCache.txt
$ cmake -GNinja ../llvm -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_FLAGS_RELEASE="-fxray-instrument -fxray-instruction-threshold=1" \
-DCMAKE_CXX_FLAGS="-fxray-instrument -fxray-instruction-threshold=1"
$ ninja llc
$ XRAY_OPTIONS="patch_premain=true" ./bin/llc input.ll
==69819==XRay: Log file in 'xray-log.llc.5rqxkU'
$ llvm-xray account xray-log.llc.5rqxkU --top=10 --sort=sum --sortorder=dsc --instr_map=./bin/llc
Functions with latencies: 36652
funcid count [ min, med, 90p, 99p, max] sum function
75 1 [ 0.672368, 0.672368, 0.672368, 0.672368, 0.672368] 0.672368 llc.cpp:271:0: main
78 1 [ 0.626455, 0.626455, 0.626455, 0.626455, 0.626455] 0.626455 llc.cpp:381:0: compileModule(char**, llvm::LLVMContext&)
139617 1 [ 0.472618, 0.472618, 0.472618, 0.472618, 0.472618] 0.472618 LegacyPassManager.cpp:1723:0: llvm::legacy::PassManager::run(llvm::Module&)
139610 1 [ 0.472618, 0.472618, 0.472618, 0.472618, 0.472618] 0.472618 LegacyPassManager.cpp:1681:0: llvm::legacy::PassManagerImpl::run(llvm::Module&)
139612 1 [ 0.470948, 0.470948, 0.470948, 0.470948, 0.470948] 0.470948 LegacyPassManager.cpp:1564:0: (anonymous namespace)::MPPassManager::runOnModule(llvm::Module&)
139607 2 [ 0.147345, 0.315994, 0.315994, 0.315994, 0.315994] 0.463340 LegacyPassManager.cpp:1530:0: llvm::FPPassManager::runOnModule(llvm::Module&)
139605 21 [ 0.000002, 0.000002, 0.102593, 0.213336, 0.213336] 0.463331 LegacyPassManager.cpp:1491:0: llvm::FPPassManager::runOnFunction(llvm::Function&)
139563 26096 [ 0.000002, 0.000002, 0.000037, 0.000063, 0.000215] 0.225708 LegacyPassManager.cpp:1083:0: llvm::PMDataManager::findAnalysisPass(void const*, bool)
108055 188 [ 0.000002, 0.000120, 0.001375, 0.004523, 0.062624] 0.159279 MachineFunctionPass.cpp:38:0: llvm::MachineFunctionPass::runOnFunction(llvm::Function&)
62635 22 [ 0.000041, 0.000046, 0.000050, 0.126744, 0.126744] 0.127715 X86TargetMachine.cpp:242:0: llvm::X86TargetMachine::getSubtargetImpl(llvm::Function const&) const
檢測屬性¶
另一種方法是使用配置文件來選擇哪些函數應該始終由編譯器檢測。這為我們提供了一種方法,可以確保某些函數始終或永不進行檢測,而無需將屬性添加到源代碼中。
要使用此功能,您可以為始終檢測的函數定義一個文件,為永不檢測的函數定義另一個文件。這些文件的格式與控制 Sanitizer 實現類似功能的 SanitizerLists 文件完全相同。例如
# xray-attr-list.txt
# always instrument functions that match the following filters:
[always]
fun:main
# never instrument functions that match the following filters:
[never]
fun:__cxx_*
給定上面的文件,我們可以通過將其提供給 clang 的 -fxray-attr-list=
標誌來重新構建。您可以有多個文件,每個文件定義不同的屬性集,由 clang 組合成單個列表。
XRay 堆棧工具¶
給定一個追蹤,以及一個可選的檢測映射,llvm-xray stack
命令可用於分析從函數調用時間線構建的調用堆棧圖。
使用該命令的方法是按調用次數和花費的時間輸出頂部堆棧。
$ llvm-xray stack xray-log.llc.5rqxkU --instr_map=./bin/llc
Unique Stacks: 3069
Top 10 Stacks by leaf sum:
Sum: 9633790
lvl function count sum
#0 main 1 58421550
#1 compileModule(char**, llvm::LLVMContext&) 1 51440360
#2 llvm::legacy::PassManagerImpl::run(llvm::Module&) 1 40535375
#3 llvm::FPPassManager::runOnModule(llvm::Module&) 2 39337525
#4 llvm::FPPassManager::runOnFunction(llvm::Function&) 6 39331465
#5 llvm::PMDataManager::verifyPreservedAnalysis(llvm::Pass*) 399 16628590
#6 llvm::PMTopLevelManager::findAnalysisPass(void const*) 4584 15155600
#7 llvm::PMDataManager::findAnalysisPass(void const*, bool) 32088 9633790
..etc..
在默認模式下,不同線程上的相同堆棧會獨立聚合。在多線程程序中,您最終可能會讓相同的調用堆棧填滿您的熱門調用列表。
要解決這個問題,您可以指定 --aggregate-threads
或 --per-thread-stacks
旗標。--per-thread-stacks
將執行緒 ID 視為每個呼叫堆疊樹中的隱含根目錄,而 --aggregate-threads
則會組合來自所有執行緒的相同堆疊。
火焰圖產生¶
llvm-xray stack
工具也可以用於產生火焰圖,以便將您已檢測的呼叫視覺化。該工具本身不會產生圖表,而是產生一種格式,可以用於 Brendan Gregg 的 FlameGraph 工具,目前可在 github 上取得。
若要產生火焰圖的輸出,還需要一些選項。
--all-stacks
- 發出所有堆疊。--stack-format
- 選擇火焰圖輸出格式「flame」。--aggregation-type
- 選擇要繪製圖表的指標。
您可以將指令輸出直接通過管道傳送到火焰圖工具,以取得 svg 檔案。
$ llvm-xray stack xray-log.llc.5rqxkU --instr_map=./bin/llc --stack-format=flame --aggregation-type=time --all-stacks | \
/path/to/FlameGraph/flamegraph.pl > flamegraph.svg
如果您在瀏覽器中開啟 svg 檔案,滑鼠事件允許您探索呼叫堆疊。
Chrome 追蹤檢視器視覺化¶
我們也可以從相同的產生追蹤中產生一個追蹤,可以由 Chrome 追蹤檢視器載入
$ llvm-xray convert --symbolize --instr_map=./bin/llc \
--output-format=trace_event xray-log.llc.5rqxkU \
| gzip > llc-trace.txt.gz
從 Chrome 瀏覽器中,瀏覽至 chrome:///tracing
允許我們載入 sample-trace.txt.gz
檔案以視覺化執行追蹤。
進一步探索¶
llvm-xray
工具還有一些處於不同開發階段的其他子指令。一個有趣且可以突顯一些有趣事情的子指令是 graph
子指令。例如,給定以下我們使用 XRay 檢測建構的玩具程式,我們可以看到產生的圖表如何成為應用程式在哪裡花費時間的有用指標。
// sample.cc
#include <iostream>
#include <thread>
[[clang::xray_always_instrument]] void f() {
std::cerr << '.';
}
[[clang::xray_always_instrument]] void g() {
for (int i = 0; i < 1 << 10; ++i) {
std::cerr << '-';
}
}
int main(int argc, char* argv[]) {
std::thread t1([] {
for (int i = 0; i < 1 << 10; ++i)
f();
});
std::thread t2([] {
g();
});
t1.join();
t2.join();
std::cerr << '\n';
}
然後,我們使用 XRay 檢測建構上述程式碼
$ clang++ -o sample -O3 sample.cc -std=c++11 -fxray-instrument -fxray-instruction-threshold=1
$ XRAY_OPTIONS="patch_premain=true xray_mode=xray-basic" ./sample
然後,我們可以探索由此範例應用程式產生的追蹤圖形渲染。我們假設您的系統中可以使用 graphviz 工具,包括 unflatten
和 dot
。如果您希望使用其他工具渲染或探索圖表,那麼這也應該是可行的。llvm-xray graph
將建立 DOT 格式的圖表,這些圖表應該可以在大多數圖形渲染應用程式中使用。llvm-xray graph
指令的一個範例呼叫應該會對 C++ 應用程式的運作方式提供一些有趣的見解
$ llvm-xray graph xray-log.sample.* -m sample --color-edges=sum --edge-label=sum \
| unflatten -f -l10 | dot -Tsvg -o sample.svg
後續步驟¶
如果您有一些有趣的分析想作為 llvm-xray 工具的一部分來實作,請隨時在 llvm-dev@ 電子郵件清單中提出。以下是一些想法,可以激勵您參與其中並可能讓事情變得更好。
實作一個查詢/過濾程式庫,允許在 XRay 追蹤中尋找模式。
收集函式呼叫堆疊以及它們在 XRay 追蹤中出現的頻率。