gprof
Summary
-
gprof 處理 call graph 的方式只考慮單一 edge, 而不是整條 call path, 導致其只能用估算的方式得知各 caller 佔用的 children time, 也無法正確判斷 recursive call
- e.g., 假設一程式含有以下 call path:
A -> C
, B -> C
, 及 C -> D
, 我們會想知道 function A 與 B 各自引發幾次 D, 以及佔用的時間比例?
- gprof 計算方式是, 假設每條 call path (edge) 發生機率都是 100%, 但忽略了部份 function 可能會提早結束的事實. 這個假設意即: A & B 一定會走到 C, 且若 C 沒有其他 callee, 則 call path
A -> C -> D
發生次數等同於 A 呼叫 C 的次數。但這個假設是錯的, 實際上 A -> C
可能只有 50% 的機率會走到 D, 或甚至 call path A -> C -> D
可能根本不存在
-
gprof 只會採樣 function self-time 及呼叫次數, 其他數值都是估計出來的, 僅供參考
- 某些需要以 function call 做為 profiling 依據的場合, 就適合用 gprof。例如程式執行時間與輸入資料量不成比例時, 就可以用 gprof 檢查 function call 次數是否異常
-
僅適用於 CPU-bound 程式
Profiling Concept
Profiling 包括以下需求
- 找出最耗時的 function, 還要區分 self time 及 children time
- 統計 caller & callee 呼叫次數
e.g., 考慮以下 call graph, 包含以下 call path:
- A -> C -> D
- B -> C -> D
- X1 -> C -> Y1
- X2 -> C -> Y2
- Z -> D
以 function C 為例, 我們會想知道:
- C 的 self seconds 總合 (含所有 caller, threads 造成的 self-seconds)
- C 的 child time 總合 (含 C -> D, Y1, Y2, …) (但不含 Z -> D)
- C 在特定 context 下的 self seconds (e.g., 不同 caller 或參數下的表現)
- C 在特定 context 下的 child time (e.g., C 被 A 呼叫時, 不會走到 Y1 或 Y2, 所以這種狀況下不用採計 Y1&Y2 的時間)
不過 gprof 僅是統計 self seconds 及呼叫次數, 估計值未必準確, 詳後述
gprof Tryout
- Compile your program with option
-pg
, e.g., $ gcc -pg -o hello hello.c
To capture all the symbols, it's better to use the -O0
option.
- Run your program, then the file
gmon.out
will be generated.
- Use
gprof
to analysis gmon.out
e.g.,
Basics
gprof Documents
https://sourceware.org/binutils/docs/gprof/index.html#Top
old version: https://ftp.gnu.org/old-gnu/Manuals/gprof-2.9.1/html_chapter/gprof_toc.html
gprof原理
https://sourceware.org/binutils/docs/gprof/Implementation.html
https://docs.freebsd.org/44doc/psd/18.gprof/paper.pdf
- The mcount routine, included in the profiling library, is responsible for recording in an in-memory call graph table both its parent routine (the child) and its parent’s parent.
- Number-of-calls information for library routines is collected by using a special version of the C library
- The output from gprof gives no indication of parts of your program that are limited by I/O or swapping bandwidth.
常用參數
gprof 輸出說明
https://sourceware.org/binutils/docs/gprof/Output.html
腦補: gprof 只是統計每個 function 佔用的 CPU time 及被呼叫次數 (self-seconds & calls), 以及 caller 呼叫 callee 的次數。
然後再由 caller 呼叫次數, 推算出每個 caller 佔用的時間比例
至於 caller self-seconds 在各 caller , children time 可能是由呼叫次數估算而來, 僅供參考
Flat profile
列出 self seconds 及呼叫次數。其他欄位也是由 self seconds 推算出來的
- self seconds: 本函式於 profiling 期間所花費的時間總和, 不含 callee 花費的時間
- self s/call: 承上, (self s/call) = self seconds / calls
- 陷阱: 未考慮參數及 caller 差異, 所以僅供參考
- total s/call: 本函式執行一次所花費的時間 (含 callee 花費的時間), 概念與 perf 的 child overhead 相同。
-
"This is the only field in the flat profile that uses call graph analysis. "
- total seconds == self seconds + all callee's self seconds introduced by this function
- 陷阱: 未考慮不同 call path 及 callee 參數的差異, 所以僅供參考
- cumulative seconds: 本列(含)以上所有函式的 self seconds 總和, 僅表示累計程式整體執行時間, 沒什麼參考價值 (所有 functions 的 self seconds 總和當然就是程式總體時間)
- 本欄位概念與 perf 的 child overhead 不同: perf 是加總 self-time 與 callee time, 但 gprof flat profile 每一列之間不見得有 caller-callee 關係
至於表格第一行是 Sampling period (SIGPROF period)
- e.g., "Each sample counts as 0.01 seconds", 表示 sampling rate = 100Hz
程式總執行時間: 2.89 sec
Call graph
本功能顯示一個函式的 self time 是由哪個 caller 引起 (一個函式可能有數個caller), 以及各 callee 花費的時間, 可用來了解 code path 整體效能
- 方括號索引開頭者為 primary line。Primary line 上面是 caller, 下面是 callee
ray_hit_object self-second + children time 佔總體時間 65.1%, 算是滿耗時的 function
先分析 ray_hit_object self seconds, 抓上游原因 (caller 呼叫次數)
- self seconds == 0.12 sec, 佔程式整體執行時間 0.04% (0.12/2.89)
- ray_hit_object 總共被呼叫 4620625 次, 全部來自於 ray_color (ray_color 該列的 self-seconds 顯示 0.12), 所以暫時先不用往上追
再來看 ray_hit_object callees, 查看最耗時的 callee
- children time == 1.77 sec (僅供參考)
- 注意, children time 是所有 reachable nodes 的 self seconds 總和, 意即相鄰節點的 self seconds (0.25 + 0.27 + 0.05 + 0.04), 加上相鄰節點的 children time (raySphereIntersection 0.45 + rayRectangularIntersection 0.71)
- 當然, 這也是用各別函式的 self seconds 以及呼叫次數推算而得
- flat view 的 total seconds 還包含 self seconds。
再來看 ray_hit_object 的 callee add_vector, 以及 add_vector 的其他 callers
- add_vector 總共被呼叫 17836094 次, total self-seconds == 0.14 sec
其中 4620625 次來自於 ray_hit_object, 大約佔其 self-seconds 0.04 sec (4620625 / 17836094 * 0.14
)
- 注意 graph 之間的關聯: 一個 edge 會顯示於 caller & callee's graph。以
ray_hit_object -> add_vector
為例:
- in ray_hit_object's graph:
0.04 0.00 4620625/17836094 add_vector [13]
- in add_vector's graph:
0.04 0.00 4620625/17836094 ray_hit_object [4]
Cycle view
Recursive functions 無法精確分割 children time, 例如 A -> B -> A
, 則 A 的 children time 是否該包含 A 本身?
gprof 的做法是把 recursive functions 視為一個 virtual function (directed graph with cycle), 其 children time 為 graph 以外的 functions
注意
- 函式構成 cycle 與否以 runtime call path 為準, 如果兩個 function 在執行期間沒有互相呼叫 (沒有 edge), 就不會構成 cycle
- 注意 cycle 僅表示兩個 node 之間存在雙向 edge (bidirected edge),但不表示程式執行期間存在 cycle path (recursive call)。這是 gprof 計量方式的缺點: gprof 沒有把整個 call path 視為一體處理。
例如以下程式包含兩條 call path: main -> f1 -> f2
及 main -> f2 -> f1
, 但沒有 f1 -> f2 -> f1
或 f1 -> f2 -> f1
, gprof 仍把 f1 & f2 視為 cycle 的一部份
- 承上,
f1 <-> f2 <-> f3
也會構成 cycle
gprof 缺點
Stackoverflow: Alternatives to gprof
Stackoverflow: gprof vs. cachegrind profiles
gprof原理与缺陷
Linux性能评测工具之一:gprof篇
- 不適用於執行時間變異量過大的 functions
gprof 假設函式每次執行時間及呼叫路徑等成本皆相同, 實際上一個 function 的 self-time 或 total-time 可能因執行環境或參數而異 (包括 caller 傳入的參數, 或 global variable)。這種平均計算方式可能會稀釋 hot-spot 的真實執行時間,導致 user 找不出 hot-spot
- e.g., 不同 caller 傳入不同參數:
f1() -> f3()
vs. f2() -> f3()
兩條 call path 的 f3() self-time 可能不同, 但 flat profile 的 self s/call 是假設任何狀況下的 self seconds 皆相同, call graph 則是以呼叫次數估算 self-seconds 來源, 兩者皆無法反應不同 caller 與參數的差異
- e.g., callee 執行時間的差異:
f3() -> f4()
vs. f3() -> f5()
兩條 call path 的 f3() total time 不同 (一個走到 f4, 一個走到 f5), 但 flat profile 假設 f4(), f5() 的執行時間都一樣
- 承上, 因為只記錄一層 parent/child 呼叫關係, 所以無法得知不同 caller 對 children time 的影響
考慮以下 callgraph: f1() -> f3() -> f4()
f2() -> f3() -> f5()
gprof 無法判斷 f4 & f5 是由 f1 or f2 引發, 導致 f3 的 children time 被平均分配到 f1 & f2
- 就算只考慮單一 call path
f1() -> f3()
, 實際程式可能因參數不同導致 f1() & f3() self-time / children time 的差異
- 無法處理 inline function or macro
- 不適用於 recursive call, multi-thread program, daemon program
- Dynamic linked library 也必須以
-pg
重新編譯
- 無法得知 CPU 在 kernel-mode 耗費時間 (syscalls, waiting for CPU or I/O waiting)
- Clock interrupt 可能造成 system call 被中斷 (EINTR) 及程式執行異常
https://sourceware.org/binutils/docs/gprof/Implementation.html
ftp://ftp.gnu.org/old-gnu/Manuals/gprof-2.9.1/html_chapter/gprof_9.html
Example: 估算錯誤的 caller self-seconds 及 callee self-seconds per call
f1 的 兩個 caller 會引發不同的 self-seconds
Flat profile: gprof 無法辨識 caller 傳入參數的差異, f1() self-seconds per call 僅為平均值
Call graph: gprof 無法辨識 caller 傳入參數的差異, 因此 f1 的 self-seconds 被均分至 f2 & f3
gprof 由 f1 呼叫次數推斷 f2 和 f3 的 children time 都一樣, 這就是悲劇的開始。這個錯誤假設會一路 propagate 到所有的 caller, 例如 f2 和 f3 的 call graph 和 total s/call 都是錯的, 導致我們看不出 f3 才是真正花時間的 function
Example: 估算錯誤的 caller children time
f3() 的兩個 caller 會引發不同的 children time
如同上述, gprof 無法區分 f4, f5 是由 f1 或 f2 引發
gprof 假設 f3 每次執行成本都一樣, 所以 f3 children time 被均分至 f1 & f2
最糟的例子是 f1 根本不會走到 f4/f5, 但 gprof 仍誤把 children time 累加至所有 caller, 導致某些 caller 得到不正確的 total time, 實際上這些函式不是 hot spot
Case Studies
Profiling 重點
- total seconds == self seconds + all the callee's self seconds
- 用 flat profile 找出最花時間的 function A
- 用 call graph 找出 function A 的 caller functions B, C, D, 釐清整個 call path
- 用 call graph 找出 caller functions 有無其他較耗時的 callees
Example: 2016q3 hw2 raytracing
- 在 flat profile 裡, multiply_vector() 被呼叫 31410180 次 self second = 0.93 秒, 但我們想知道進一步知道這 0.93 秒是由哪個 caller 引起的
- 由 Call graph 可得知, localColor() 呼叫 multiply_vector() 8442304 次, 佔了 0.93 秒當中的 0.25 秒 (大約等於
0.93 * 8442304 / 31410180
)
- 往下看 localColor() 的 call graph: 呼叫 callees 共花費 0.79s (包括 multiply_vector() 的 0.25s)
Flat profile:
Call graph:
References
https://sourceware.org/binutils/docs/gprof/index.html
https://users.cs.duke.edu/~ola/courses/programming/gprof.html
SIGPROF
https://blog.csdn.net/wallwind/article/details/50393546
http://www.berlinix.com/dev/gprof.php