用 CPI 火焰圖分析 Linux 性能問題
雲棲君導讀:本文介紹了使用 CPI 火焰圖分析程序性能的方法。CPI 火焰圖不但展示了程序的 Call Stack 與 CPU 佔用率的關聯性,而且還揭示了這些 CPU 佔用率里,哪些部分是真正的有效的運行時間,哪些部分實際上是 CPU 因某些停頓造成的忙等。
1. 什麼是 CPI ?
本小節講述為什麼使用 CPI 分析程序性能的意義。如果已經非常了解 CPI 對分析程序性能的意義,可以跳過本小節的閱讀。
1.1 程序怎麼樣才能跑得快 ?
理解什麼是 CPI,首先讓我們思考一個問題:在一個給定的處理器上,如何才能讓程序跑得更快呢?
假設程序跑得快慢的標準是程序的執行時間,那麼程序執行的快慢,就可以用如下公式來表示:
因此,要想程序跑得快,即減少程序執行時間,我們就需要在以下三個方面下功夫:
減少程序總指令數
要減少程序執行的總指令數,可能有以下手段:
演算法優化;好的演算法設計,可能帶來更少的指令執行數
更高效的編譯器或者解釋器;新的編譯器或者解釋器,可能對同樣的源代碼,生成更少的機器碼。
用更底層的語言優化;這是為何 Linux 內核代碼使用 C 語言,並且還喜歡內聯彙編。
更新的處理器指令;新的處理器指令,對處理某類特殊目的運算更有幫助,而新版本編譯器最重要的工作就是,在新的處理器上,用最新的高效指令;例如,x86 SSE,AVX 指令。、
減少每 CPU 時鐘周期時間
這一點很容易理解,縮短 CPU 時鐘周期的時間,實際上就是要提高 CPU 的主頻。這正是 Intel 過去占無不勝的法寶之一。今天,由於主頻的提高已經到了製造工藝的極限,CPU 時鐘周期的時間很難再繼續降低了。
減少每指令執行所需平均時鐘周期數
如何減少每指令執行所需平均 CPU 時鐘周期數呢?讓我們先從 CPU 設計角度看一下:
標量處理器 (Scalar Processor) ;一個 CPU 時鐘周期只能執行一條指令;
超標量處理器 (Superscalar Processor);一個 CPU 時鐘周期可以執行多條指令;通常這個是靠在處理器里實現多級流水線 (Pipeline) 來實現的。
因此不難看出,如果使用支持超標量處理器的 CPU,利用 CPU 流水線提高指令並行度,那麼就可以達到我們的目的了。流水線的並行度越高,執行效率越高,那麼每指令執行所需平均時鐘周期數就會越低。
當然,流水線的並行度和效率,又取決於很多因素,例如,取值速度,訪存速度,指令亂序執行 (Out-Of-Order Execution),分支預測執行 (Branch Prediction Execution),投機執行 (Speculative Execution)的能力。一旦流水線並行執行的能力降低,那麼程序的性能就會受到影響。關於超標量處理器,流水線,亂序執行,投機執行的細節,這裡不再一一贅述,請查閱相關資料。
另外,在 SMP,或者多核處理器系統里,程序還可以通過並行編程來提高指令的並行度,因此,這也是為什麼今天在 CPU 主頻再難以提高的情況下,CPU 架構轉為 Multi-Core 和 Many-Core。
由於提高 CPU 主頻的同時,又要保障一個 CPU 時鐘周期可以執行更多的指令,因此需要處理器廠商需要不斷地提高製造工藝,降低 CPU 的晶元面積和功耗。
1.2 CPI 和 IPC
在計算機體系結構領域,經常可以看到 CPI 的使用。CPI 即 Cycle Per Instruction 的縮寫,它的含義就是每指令周期數。此外,在一些場合,也可以經常看到 IPC,即 Instruction Per Cycle 的,含義為每周期指令數。
因此不難得出,CPI 和 IPC 的關係為,
使用 CPI 這個定義,本文開篇用于衡量程序執行性能的公式實際上可以表示為:
由於受到硅材料和製造工藝的限制,處理器主頻的提高已經面臨瓶頸,因此,程序性能的提高,主要的變數在 Instruction Count 和 CPI 這兩個方面。
在 Linux 上,通過 perf 工具,通過 Intel 處理器提供的特殊寄存器,可以很容易測量一個程序的 IPC。
例如,下例就可以給出 Java 程序的 IPC,8 秒多的時間裡,這個 Java 程序的 IPC 是 0.54:
那麼,通過 IPC,我們也可以換算出 CPI 是 1/0.54,約為 1.85.
通常情況下,通過 CPI 的取值,我們可以大致判斷一個計算密集型任務,到底是 CPU bound 還是 Memory Bound:
CPI 小於 1,程序通常是 CPU Bound;
CPI 大於 1,程序通常是 Memory Bound;
1.3 重新認識 CPU 利用率
對程序員來說,判斷一個計算密集型任務運行效率的重要依據就是看程序運行時的 CPU 利用率。很多人認為 CPU 利用率高就是程序的代碼在瘋狂運行。實際上,CPU 利用率高,也有可能是 CPU 正在忙等一些資源,如訪問內存遇到了瓶頸。
一些計算密集型任務,在正常情況下,CPI 很低,性能原本很好。CPU 利用率很高。但是隨著系統負載的增加,其它任務對系統資源的爭搶,導致這些計算任務的 CPI 大幅上升,性能下降。而此時,很可能 CPU 利用率上看,還是很高的,但是這種 CPU 利用率的高,實際上體現的是 CPU 的忙等,及流水線的停頓帶來效應。
至此,相信讀者已經清楚,在不修改二進位程序的前提下,通過 CPI 指標了解程序的運行性能,有著非常重要的意義。對於計算密集型的程序,只通過 CPU 利用率這樣的傳統指標,也無法幫助你確認你的程序的運行效率,必須將 CPU 利用率和 CPI/IPC 結合起來看,確定程序的執行效率。
1.4 如何分析 CPI/IPC 指標異常?
雖然利用 perf 可以很方便獲取 CPI/IPC 指標,但是想分析和優化程序高 CPI 的問題,就需要一些工具和分析方法,將 CPI 高的原因,以及與之關聯的軟體的調用棧找到,從而決定優化方向。
關於 CPI 高的原因分析,在 Intel 64 and IA-32 Architectures Optimization Reference Manual, 附錄 B 里有介紹。其中主要的思路就是按照自頂向下的方法,自頂向下排查, 4 種引起 CPI 變高的主要原因,
我們稍後會在另一篇文章介紹這種分析方法,本文主要關注使用 CPI 火焰圖來分析 CPI 的問題。
2. CPI 火焰圖
Brendan Gregg 在 CPI Flame Graphs: Catching Your CPUs Napping 一文中,介紹了使用 CPI 火焰圖來建立 CPI 和軟體調用棧的關聯。
我們已經知道,光看 CPU 利用率並不能知道 CPU 在幹嘛。因為 CPU 可能執行到一條指令就停下來,等待資源了。這種等待對軟體是透明的,因此從用戶角度看,CPU 還是在被使用狀態,但是實際上,指令並沒有有效地執行,CPU 在忙等,這種 CPU 利用率並不是有效的利用率。
要發現 CPU 在 busy 的時候實際上在幹什麼,最簡單的方法就是測量平均 CPI。CPI 高說明運行每條指令用了更多的周期。這些多出來的周期裡面,通常是由於流水線的停頓周期 (Stalled Cycles) 造成的,例如,等待內存讀寫。
而 CPI 火焰圖,可以基於 CPU 火焰圖,提供一個可視化的基於 CPU 利用率和 CPI 指標,綜合分析程序 CPU 執行效率的方案。
下面這個 CPI 火焰圖引用自 Brendan Gregg 博客文章。可以看到,CPI 火焰圖是基於 CPU 火焰圖,根據 CPI 的大小,在每個條加上了顏色。紅色代表指令,藍色代表流水線的停頓:
火焰圖中,每個函數幀的寬度,顯示了函數或其子函數在 CPU 上的次數,和普通 CPU 火焰圖完全一樣。而顏色則顯示了函數此時在 CPU 上是運行 (running 紅色) 還是停頓 (stalled 藍色)。
火焰圖裡,顏色範圍,從最高CPI為藍色(執行最慢的指令),到最低CPI為紅色 (執行最快的指令)。火焰圖是 SVG 格式,矢量圖,因此支持滑鼠點擊縮放。
然而,Brendan Gregg 博客中的這篇博客,CPI 火焰圖是基於 FreeBSD 操作系統特有的命令生成的,而在 Linux 上,應該怎麼辦呢?
3. 一個小程序
讓我們寫一個人造的小程序,展示在 Linux 下 CPI 火焰圖的使用。
這是一個最簡的小程序,其中包含如下兩個函數:
cpu_bound
memory_bound
下面是程序的源碼:
在上述小程序運行時,我們使用如下命令生成 CPI 火焰圖,
$ perf record -e cpu/event=0xa2,umask=0x1,name=resource_stalls_any,period=2000003/ -e cpu/event=0x3c,umask=0x0,name=cpu_clk_unhalted_thread_p,period=2000003/ --call-graph dwarf -F 200 ./cpu_and_mem_bound
$ perf script > out.perf
$ FlameGraph/stackcollapse-perf.pl --event-filter=cpu_clk_unhalted_thread_p out.perf > out.folded.cycles
$ FlameGraph/stackcollapse-perf.pl --event-filter=resource_stalls_any out.perf > out.folded.stalls
$ FlameGraph/difffolded.pl -n out.folded.stalls out.folded.cycles | FlameGraph/flamegraph.pl --title "CPI Flame Graph: blue=stalls, red=instructions" --width=900 > cpi_flamegraph_small.svg
最後生成的火焰圖如下,
可以看到,CPI 火焰圖看到的結果,是符合我們的預期的:
該程序所有的 CPU 時間,都分布在 cpu_bound 和 memory_bound 兩個函數里
同是 CPU 佔用時間,但 cpu_bound 是紅色的,代表這個函數的指令在 CPU 上一直持續運行
而 memory_bound 是藍色的,代表這個函數發生了嚴重的訪問內存的延遲,導致了流水線停頓,屬於忙等
4. 一個benchmark
現在,我們可以使用 CPI 火焰圖來分析一個略真實一些的測試場景。下面的 CPI 火焰圖,來自 fio的測試場景。
這個 fio 對 SATA 磁碟,做多進程同步 Direct IO 順序寫,可以看到:
紅顏色為標記為 CPU Bound 的函數。其中顏色最深的是 _raw_spin_lock,這是自旋鎖的等待循環引起的。
藍顏色為標記為 Memory Bound 的函數。其中顏色最深的是 fio 測試程序的函數 get_io_u,如果使用 perf 程序進一步分析,這個函數里發生了嚴重的 LLC Cache Miss。
因為 CPI 火焰圖是矢量圖,支持縮放,所以以上結論可以通過放大 get_io_u 的調用棧進一步確認,
到這裡,讀者會發現,使用 CPI 火焰圖,可以很方便地做 CPU 利用率的分析,找到和定位引發 CPU 停頓的函數。一旦找到相關的函數,就可以通過 perf annotate 命令對引起停頓的指令作出進一步確認。並且,我們可以利用 1.4 小節的自頂向下分析方法,對 CPU 哪個環節產生瓶頸作出判斷。最後,結合這些信息,決定優化方向。
5. 小結
本文介紹了使用 CPI 火焰圖分析程序性能的方法。CPI 火焰圖不但展示了程序的 Call Stack 與 CPU 佔用率的關聯性,而且還揭示了這些 CPU 佔用率里,哪些部分是真正的有效的運行時間,哪些部分實際上是 CPU 因某些停頓造成的忙等。
系統管理員可以通過此工具發現系統存在的資源瓶頸,並且通過一些系統管理命令來緩解資源的瓶頸;例如,應用間的 Cache 顛簸干擾,可以通過將應用綁到不同的 CPU 上解決。
而應用開發者則可以通過優化相關函數,來提高程序的性能。例如,通過優化代碼減少 Cache Miss,從而降低應用的 CPI 來減少處理器因訪存停頓造成的性能問題。
由於本文主要是介紹 CPI 火焰圖,對於 1.4 小節提到的自頂向下的分析方法,限於篇幅所限,這裡不詳細展開了。關於此內容,我們稍後會有專門的文章做詳細介紹。
end
※一個小例子帶你輕鬆Keras圖像分類入門
※如何應對數據科學的「負擔症候群」
TAG:雲棲社區 |