layout:
Top-down Microarchitecture Analysis Method
原文連結
1,https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf
2,https://software.intel.com/en-us/vtune-amplifier-help-tuning-applications-using-a-top-down-microarchitecture-analysis-method
3,http://halobates.de/blog/p/262
翻譯: 棄餘
引言
現代 CPU 大多具有效能監控單元(Performance Monitoring Unit, PMU),用於統計系統中發生的特定硬體事件,例如快取未命中(Cache Miss)或者分支預測錯誤(Branch Misprediction)等。同時,多個事件可以結合計算出一些高階指標,例如每指令週期數(CPI),快取命中率等。一個特定的微體系架構可以透過 PMU 提供數百個事件。對於發現和解決特定的效能問題,我們很難從這數百個事件中挑選出那些真
正有用的事件。 這需要我們深入瞭解微體系架構的設計和 PMU 規範,才能從原始事件資料中獲取有用的資訊。
自頂向下的微體系架構分析方法(Top-Down Microarchitecture Analysis Method, TMAM)可以在亂序執行的核心中識別效能瓶頸,其通用的分層框架和技術可以應用於許多亂序執行的微體系架構。TMAM 是基於事件的度量標準的分層組織,用於確定應用程式中的主要效能瓶頸,顯示執行應用程式時 CPU 流水線的使用情況。
概述
現代高效能 CPU 的流水線非常複雜。 一般來說,CPU 流水線在概念上分為兩部分,即前端(Front-end)和後端(Back-end)。Front-end 負責獲取程式程式碼指令,並將其解碼為一個或多個稱為微操作(uOps)的底層硬體指令。uOps 被分配給 Back-end 進行執行,Back-end 負責監控 uOp 的資料何時可用,併在可用的執行單元中執行 uOp。 uOp 執行的完成稱為退役(Retirement),uOp 的執行結果提交並反饋到>架構狀態(CPU 暫存器或寫回記憶體)。 通常情況下,大多數 uOps 透過流水線正常執行然後退役,但有時候投機執行的 uOps 可能會在退役前被取消,例如在分支預測錯誤的情況下。
在最近的英特爾微體系結構上,流水線的 Front-end 每個 CPU 週期(cycle)可以分配4個 uOps ,而 Back-end 可以在每個週期中退役4個 uOps。 流水線槽(pipeline slot)代表處理一個 uOp 所需的硬體資源。 TMAM 假定對於每個 CPU 核心,在每個 CPU 週期內,有4個 pipeline slot 可用,然後使用專門設計的 PMU 事件來測量這些 pipeline slot 的使用情況。在每個 CPU 週期中,pipeline slot 可以是空
的或者被 uOp 填充。 如果在一個 CPU 週期內某個 pipeline slot 是空的,稱之為一次停頓(stall)。如果 CPU 經常停頓,系統效能肯定是受到影響的。TMAM 的標的就是確定系統效能問題的主要瓶頸。
下圖展示並總結了亂序執行微體系架構中自頂向下確定效能瓶頸的分類方法。這種自頂向下的分析框架的優點是一種結構化的方法,有選擇地探索可能的效能瓶頸區域。 帶有權重的層次化節點,使得我們能夠將分析的重點放在確實重要的問題上,同時無視那些不重要的問題。
例如,如果應用程式效能受到指令提取問題的嚴重影響, TMAM 將它分類為 Front-end Bound 這個大類。 使用者或者工具可以向下探索並僅聚焦在 Front-end Bound 這個分類上,直到找到導致應用程式效能瓶頸的直接原因或一類原因。
設計
Top Level
在最頂層,TMAM 將 pipeline slot 分為四個主要類別:
-
Front-end Bound
1Front-end Bound 表示 pipeline 的 Front-end 不足以供應 Back-end。Front-end 是 pipeline 的一部分,負責交付 uOps 給 Back-end 執行。Front-end Bound 進一步分為 Fetch Latency(例如,ICache or ITLB misses)和 Fetch Bandwidth(例如,sub-optimal decoding)。
-
Back-end Bound
1Back-end Bound 表示由於缺乏接受執行新操作所需的後端資源而導致停頓的 pipeline slot 。它進一步分為分為 Memory Bound (由於記憶體子系統造成的執行停頓)和 Core Bound(執行單元壓力 Compute Bound 或者缺少指令級並行 ILP)。
-
Bad Speculation
1Bad Speculation 表示由於分支預測錯誤導致的 pipeline slot 被浪費,主要包括 (1) 執行最終被取消的 uOps 的 pipeline slot,以及 (2) 由於從先前的錯誤猜測中恢復而導致阻塞的 pipeline slot。
-
Retiring
1Retiring 表示執行有效 uOp 的 pipeline slot。 理想情況下,我們希望看到所有的 pipeline slot 都能歸類到 Retiring,因為它與 IPC 密切相關。 儘管如此,高 Retiring 比率並不意味著沒有提升最佳化的空間。
後兩者表示非停頓的 pipeline slot,前兩者表示停頓的 pipeline slot。 下圖描述了一個簡單的決策樹來展示向下分析的過程。如果一個 pipeline slot 被某個 uOp 使用,它將被分類為 Retiring 或 Bad Speculation,具體取決於它是否最終提交。如果 pipeline 的 Back-end 部分不能接受更多操作(也稱為 Back-end Stall),未使用的 pipeline slot 被分類為 Back-end Bound。Front-end Bound 則表示>在沒有 Back-end Stall 的情況下沒有操作(uOps)被分配執行。
Front-end Bound
在許多情況下,Front-end 指令頻寬可能會影響效能,特別是在高 IPC 的情況下。一些專用單元被引入,用來隱藏流水線 Fetch 指令延遲以及維持所需的頻寬,例如 Loop Stream Detector (LSD) 以及 Decoded I-cache (DSB)。
TMAM 進一步將 Front-end Bound 劃分為延遲和頻寬兩個子類:
-
ICache miss 屬於 Fetch Latency 分類
-
指令解碼器的低效問題屬於 Fetch Bandwidth 分類
這些度量標準都是以自頂向下的方式定義的。Fetch Latency 表示任何原因導致的指令提取饑餓(沒有指令輸送)。我們所熟知的 icache and i-TLB miss 就屬於這個類別,但是並不侷限於此。Branch Resteers 表示流水線掃清(pipeline flush)之後的指令提取延遲。pipeline flush 可能由一些清除狀態的事件引起,例如 branch misprediction 或者 memory nukes。Branch Resteers 與 Bad Speculation 密切
相關。
Back-end Bound
Back-end Bound 分為 Memory Bound 和 Core Bound,透過在每個週期內基於執行單元的佔用情況來分析 Back-end 停頓。為了達到盡可能大的 IPC,需要使得執行單元保持繁忙。例如,在一個有4個 slot 的機器中,如果在穩定狀態下只能執行三個或更少的 uOps,就不能達到最佳狀態,即 IPC 等於4。這些次優週期稱為 Execution Stalls。
-
Memory Bound
1Memory Bound 對應快取和記憶體子系統相關的 Execution Stalls。這些停頓通常表現為執行單元在短時間內饑餓,例如 load 操作沒有在快取中命中。 對於常見情況,記憶體訪問的真正代價是排程程式沒有其他準備好的 uOps 提供給執行單元。後面的 uOps 可能正在等待進行中的記憶體訪問,或者依賴於其他未準備好的 uOps。
2
3Execution Stalls 包含幾個子類,每個子類都與特定的高速快取級別相關聯,取決於各個高速快取級別是否可以滿足所需的資料。在某些情況下,Execution Stall 可能會經歷顯著的延遲,遠遠大於相應快取級別的標準延遲,即使沒有發生相應的快取未命中。例如,L1D 高速快取通常具有與 ALU 停頓相當的較短的延遲。 然而在某些情況下,如 load 操作被阻塞,無法將資料從較早的 store 操作轉發(forward )到一個重疊地址,這個 load 負載可能會遭受較高的延遲,雖然最終能在 L1D 中命中。 在這種情況下,in-flight 的 load 操作將持續很長時間並且不會產生 L1D miss。因此,這個問題屬於 L1 Bound 子類。
4
5此外,與 store 操作相關的 Execution Stalls 都屬於 Stores Bound 子類。由於記憶體訪問順序要求,store 操作被快取並非同步執行。通常, store 操作對效能影響很小,但不能完全忽視。TMAM 將 Stores Bound 定義為那些執行埠利用率(execution port utilization)較低,以及存在大量需要消耗資源用來緩衝 store 操作的週期。
6
7最後,TMAM 在 Ext. Memory Bound 子類下使用了一個簡單的啟髮式演演算法來區分 MEM Bandwidth 和 MEM Latency。該啟髮式演演算法的主要根據是當前有多少請求依賴從記憶體中獲取的資料。每當這類請求的佔用率超過一個高閾值時(例如最大請求數的70%),TMAM 將其標記為可能受記憶體頻寬的限制。其他部分都屬於記憶體延遲子類。 -
Core Bound
1Core Bound 對應於執行單元存在壓力或者程式中缺少指令級別並行(ILP)。Core bound 的停頓可能表現為較短的執行饑餓週期或者執行埠利用率不佳,這使得識別 Core bound 比較困難。 例如,一個長延遲的除法操作可能會序列化執行,而服務於特定型別 uOps 的執行埠上的壓力可能表現為一個週期內只有少量埠被使用。
2
3Core Bound 的問題一般可以透過更好的程式碼生成來緩解。例如,一系列相關的算術運算將被標記為 Core Bound。編譯器可以透過更好的指令排程來緩解這種停頓。 向量化(Vectorization)也可以緩解 Core Bound 的問題。
Bad Speculation
Bad Speculation 表示由於不正確的預測而浪費的 pipeline slot,主要包括兩部分:
-
執行了最終不會被提交的 uOps 的 slots
-
從錯誤預測中恢復而導致流水線被阻塞的 slots
TMAM 的一個關鍵原則就是將 Bad Speculation 放在了最頂層, Bad Speculation 確定了受到錯誤執行路徑影響的工作負載的比例,並反過來決定了其他類別中觀察值的準確性。TMAM 進一步將 Bad Speculation 分類為 Branch Mispredict 和 Machine Clears,這兩種情況導致的問題和 pipeline flush 相像。Branch Mispredict 主要關註如何使程式控制流對分支預測更友好,Machine Clears 則主要指出一些異常
情況,例如清除記憶體排序機(memory ordering machine clears)或者自修改程式碼(self modifying code)。
Retiring
理想情況下,我們希望看到所有的 slots 都被標記為 Retiring 類別。儘管如此,Retiring 比例高並不意味著沒有更多的效能提升空間。諸如 Floating Point Assists (FP_ASSISTS) 的微指令(Microcode)序列通常會影響效能並且可以避免。這類情況被標記為 MSROM 子類以便引起註意。
非向量化(non-vectorized)程式碼的高 Retiring 比值可能是進行向量化(vectorization)程式碼的一個重要提示。這樣做基本上可以讓更多的操作透過單指令 uOp 完成,從而提高效能。TMAM 進一步將 Retiring->Base 子類劃分為 FP Arith,並區分標量操作和向量操作。
應用/工具
pmu-tools 是 Adni Kleen 開發的開源工具包,針對 Intel CPU 提供友好的介面來訪問原始事件,並提供一些附加功能。toplev 是 pmu-tools 中的一個工具,在 Intel CPU 的 Linux perf 基礎上實現了 TMAM 方法。toplev 可以定位 CPU Bound 程式碼的瓶頸,不能識別其他(Not bound by CPU)程式碼的瓶頸。toplev 是一個計數工具,它使用 PMU 來計數事件。toplev 的一個典型使用場景是,使用者已經根據一個標>準工具(例如 perf, sysprof, pyprof)進行取樣,瞭解 hot code 的分佈,但是你想知道為什麼這部分程式碼執行很慢。
安裝
toplev 在 Linux 上執行,需要安裝 perf 工具。toplev 還需要訪問 PMU,在 VM 中執行時需要註意啟用這個特性。註意,toplev 需要禁用 NMI watchdog,並以 root 身份執行。
1% git clone https://github.com/andikleen/pmu-tools
2% cd pmu-tools
3% export PATH=$PATH:`pwd`
4% sudo sysctl -p 'kernel.nmi_watchdog=0'
確定 CPU Bound 任務
第一步是確定程式是否真的是 CPU Bound 型工作負載。toplev 只能幫助定位解決 CPU Bound 問題。如果瓶頸在其他地方,則必須使用其他方法。非 CPU 瓶頸可以是網路,磁碟IO,顯示卡等。
選擇要計數的程式碼
一般來說toplev測量整個系統的效能資料;當指定一個工作負載時,toplev將在工作負載執行的時間段內測量整個系統,這一點和perf的使用是類似的。
1% toplev.py my-workload
2或者
3% toplev.py sleep XXX
讓我們衡量一個簡單的工作負載。這是一個 bc 運算式,在作者電腦上執行大約1秒(在大多數情況下,使用長時間執行的工作負載可能會更好),使用第一層級(-lxxx 引數用來設定測量的最大層級)執行以避免任何 PMU 計數器的多路復用。
1% toplev.py -l1 bash -c 'echo "7^199999" | bc > /dev/null'
2Will measure complete system.
3Using level 1.
4...
5C0 BAD Bad_Speculation: 31.66%
6 This category reflects slots wasted due to incorrect
7 speculations, which include slots used to allocate uops that
8 do not eventually get retired and slots for which allocation
9 was blocked due to recovery from earlier incorrect
10 speculation...
11C1 FE Frontend_Bound: 42.46%
12 This category reflects slots where the Frontend of the
13 processor undersupplies its Backend...
14C1 BE Backend_Bound: 27.25%
15 This category reflects slots where no uops are being
16 delivered due to a lack of required resources for accepting
17 more uops in the Backend of the pipeline...
18C0-T0 CPU utilization: 0.00 CPUs
19 Number of CPUs used...
20C0-T1 CPU utilization: 0.00 CPUs
21C1-T0 CPU utilization: 0.00 CPUs
22C1-T1 CPU utilization: 0.00 CPUs
每當首次列印層節點時,toplev 都會列印一個描述。預設情況下,它顯示一個簡短描述,長描述可以使用 --long-desc
來啟用。在之後的例子中,我們禁用描述以獲得較短的輸出。toplev 輸出中,一些值以 core 為單位,另一些則以 thread 為單位收集。多 socket 的情況下還會有 socket 分類。
上面的例子中,我們沒有將工作負載(bc)系結到某個 CPU,所以不清楚 C0 或 C1 的值是否相關。由於 bc 是單執行緒的,我們可以將它系結到一個已知的 CPU 核心,並使用 --core
來過濾該核心上的輸出。
1% toplev.py --core C0 --no-desc -l1 taskset -c 0 bash -c 'echo "7^199999" | bc > /dev/null'
2Will measure complete system.
3Using level 1.
4...
5C0 BAD Bad_Speculation: 33.29%
6C0-T0 CPU utilization: 0.00 CPUs
7C0-T1 CPU utilization: 0.00 CPUs
可以結合 taskset 系結到更多的 CPU 進行多執行緒工作,並將結果進行過濾。結果顯示 bc
受限於 Bad Speculation。現在我們可以選擇更多的節點並更詳細地分析問題。
如果已知工作負載是單執行緒的,並且系統當前空閑,那麼也可以顯式指定--single-thread
選項來測量工作負載,而不是預設測量整個系統
1% toplev.py --no-desc --single-thread bash -c 'echo "7^199999" | bc > /dev/null'
2..
3BAD Bad_Speculation: 32.65%
4CPU utilization: 0.00 CPUs
程式在初始化階段的行為相比生命週期後期的行為有很大的差異。為了精確測量,跳過這個階段通常是有用的。這可以用 -D xxx
選項來完成,xxx是跳過的毫秒數(需要較新版本的 perf)。當程式執行時間足夠長時,這通常是不需要的,但是它有助於提高小測試的精度。
預設情況下,toplev 同時測量內核和使用者程式碼。如果只對使用者程式碼感興趣,則可以使用 --user
選項。這往往會減少測量噪聲,因為中斷被過濾掉了。還有一個--kernel
選項用來測量核心程式碼。
在具有多個階段的複雜工作負載上,測量間隔也是有用的。這可以用 -I xxxi
選項指定,xxx
是間隔的毫秒數。perf 要求時間間隔至少需要 100ms。toplev 將輸出每個間隔的測量值。這往往會產生大量的資料,所以繪製輸出很有必要。
選擇正確的層次和多路復用
PMU 只有有限數量的計數器可以同時測量事件。任何多於一個層次的 toplev 執行,或者啟動了額外的CPU 指標,則需要更多的計數器。在這種情況下,核心驅動程式將開始多路復用(Multiplexing),並定期更改事件組(在1毫秒和10毫秒之間,通常2.5毫秒,取決於內核配置)。多路復用可能會導致測量錯誤,因為 toplev 中的幾個節點中的公式需要關聯多個事件組的資料。因此 toplev 在反覆執行同樣事情的工
作負載上效果最好,但在執行許多不同的短事件的工作負載上效果不佳。
只要沒有使用 PMU 的或者有問題的其他工作負載處於活動狀態,則第一層次(-l1
)和未啟用額外指標的 toplev 不會進行多路復用。一開始的時候,不採用多路復用來進行分析通常是一個好主意。更高的層次和指標提供了額外的資訊,但也增加了復用,因此可能導致更多的測量錯誤。如果工作負載非常重覆,可以使用 --no-multiplex
關閉復用。toplev 會根據需要多次重新執行工作量。在 BIOS 中禁用超執行緒
將使通用計數器的數量增加一倍,並減少多路復用。
有關問題的更多詳細資訊和解決方法,可以參閱 reasons for measuring issues
陣列求和實體的測試分析
我們考慮測量一些 beating the compiler 的例子。beating the compiler 實現了一個簡單的問題,即陣列求和,從高階指令碼語言開始,然後利用底層操作逐步最佳化。測試程式碼執行在啟用了 Turbo 的 Intel Core i7-4600U(Haswell)膝上型電腦上。
開始是簡單直接的Python實現。
1def sum_naive_python():
2 result = 0
3 for i in data:
4 result += i
5 return result
我們用 toplev 來執行這段程式碼,跳過初始化階段(大約80毫秒,透過預先測量得到)。一般來說,測量太短的程式是很困難的(太多的其他影響佔主導地位)。在這種情況下,我們透過迭代5000次測試來除錯程式執行至少幾秒鐘。
1% toplev.py -D 80 -l1 --no-desc --core C0 taskset -c 0 python first.py numbers
2..
3C0 FE Frontend_Bound: 22.08%
4C0 RET Retiring: 75.01%
所以 Python 是一點 Front-end Bound,但是從 toplev 中沒有其他發現可見的問題。我們可以透過將層級提高到3來更仔細地分析 Front-end Bound。註意這可能有缺點,因為它會導致多路復用。在這種情況下,工作負載執行時間越長越好(我們將基準函式迴圈5000次)。
1% toplev.py -D 80 -l3 --core C0 taskset -c 0 python first.py numbers
2...
3C0 FE Frontend_Bound: 21.91%
4C0 FE Frontend_Bound.Frontend_Bandwidth: 15.91%
5C0 FE Frontend_Bound.Frontend_Bandwidth.DSB: 32.11%
6 This metric represents Core cycles fraction in which CPU was
7 likely limited due to DSB (decoded uop cache) fetch
8pipeline...
9C0 RET Retiring: 74.97%
10C0 RET Retiring.Base: 74.88%
可以觀察到 Front-end Bound 是 DSB (decoded uop cache) fetch。具體描述被簡化了,可以使用 --long-desc
來檢視更具體的描述。
讓我們來看看第二個 Python 版本。這個版本使用 Python 中的內建函式 sum() 來陣列求和,以便將更多的執行動作從直譯器推送到 Python C 核心。
1def sum_builtin_python():
2 return sum(data)
在這種情況下,我們知道 python 程式碼是單執行緒的(系統的其餘部分是空閑的),所以可以使用 --single-thread
。
1% toplev.py --single-thread -l3 -D 80 python second.py numbers
2...
3FE Frontend_Bound: 27.40%
4FE Frontend_Bound.Frontend_Bandwidth: 23.20%
5FE Frontend_Bound.Frontend_Bandwidth.DSB: 46.30%
6 This metric represents Core cycles fraction in which CPU was
7 likely limited due to DSB (decoded uop cache) fetch
8 pipeline...
然而這並沒有改變多少結果。Python 是相當重(heavy-weight)的,大大加重了 CPU 的前端,但其中大部分至少在解碼的 icache 中執行。
現在我們來看一個標準的C實現,它應該快得多:
1int sum_simple(int* vec, size_t vecsize)
2{
3 int res = 0;
4 int i;
5 for (i = 0; i 6 res += vec[i];
7 }
8 return res;
9}
這個迴圈被編譯成一個簡單的測試工具,使用 gcc 4.8.3 並關閉最佳化,使用 toplev 進行測量:
1% toplev.py -l1 --single-thread --force-events ./c1-unoptimized numbers
2BE Backend_Bound: 60.34%
3 This category reflects slots where no uops are being
4 delivered due to a lack of required resources for accepting
5 more uops in the Backend of the pipeline...
這個版本比 Python 版本執行速度快4倍。瓶頸已經完全進入 Back-end。我們可以在第三層級更仔細地看待它:
1% toplev.py -l3 --single-thread --force-events ./c1-unoptimized numbers
2BE Backend_Bound: 60.42%
3BE/Mem Backend_Bound.Memory_Bound: 32.23%
4BE/Mem Backend_Bound.Memory_Bound.L1_Bound: 32.44%
5 This metric represents how often CPU was stalled without
6 missing the L1 data cache...
7 Sampling events: mem_load_uops_retired.l1_hit:pp,mem_load_uops_retired.hit_lfb:pp
8BE/Core Backend_Bound.Core_Bound: 45.93%
9BE/Core Backend_Bound.Core_Bound.Ports_Utilization: 45.93%
10 This metric represents cycles fraction application was
11 stalled due to Core computation issues (non divider-
12 related)...
可以看到它是 L1 Bound 和 Core Bound。 L1 Bound 可能是因為未最佳化的 gcc 程式碼傾向於將所有變數儲存在堆疊上,沒有進行全面的暫存器最佳化。 我們可以用 -O2
開啟最佳化器,看看會發生什麼:
1% toplev.py -l3 --single-thread ./c1-o2 numbers
2RET Retiring: 83.66%
3RET Retiring.Base: 83.62%
4 This metric represents slots fraction where the CPU was
5 retiring uops not originated from the microcode-sequencer...
6 Sampling events: inst_retired.prec_dist:pp
L1 Bound 完全消失,工作負載的大部分時間都在 Retire,這是很好的。這個版本也比未最佳化的C版本快了85%。註意這些好處有些極端的情況,可能完全取決於程式碼的行為。
最佳化 Retiring 的一種方法是對程式碼進行向量化(Vectorization),併在每條指令上做更多的工作。透過gcc -O3
啟用向量化。不幸的是,它不能向量化我們簡單的迴圈。
1c1.c:9: note: not vectorized: not suitable for gather load _32 = *_31;
我們可以從 beating the compiler 中嘗試Roguelazer手動最佳化的內嵌彙編 AVX2 版本。這應該會減少 Retiring,因為它在每個 SIMD 指令中可以執行多達8個加法,同時它還使用了迴圈展開。
1% toplev.py -l3 --single-thread ./c-asm numbers
2BE Backend_Bound: 64.15%
3BE/Mem Backend_Bound.Memory_Bound: ...
4BE/Mem Backend_Bound.Memory_Bound.L1_Bound: 49.32%
5 This metric represents how often CPU was stalled without
6 missing the L1 data cache...
7 Sampling events: mem_load_uops_retired.l1_hit:pp,mem_load_uops_retired.hit_lfb:pp
8BE/Mem Backend_Bound.Memory_Bound.L3_Bound: 48.68%
9 This metric represents how often CPU was stalled on L3 cache
10 or contended with a sibling Core...
11Sampling events: mem_load_uops_retired.l3_hit:pp
12BE/Core Backend_Bound.Core_Bound: 28.27%
13BE/Core Backend_Bound.Core_Bound.Ports_Utilization: 28.27%
14 This metric represents cycles fraction application was
15 stalled due to Core computation issues (non divider-
16 related)...
Retiring 瓶頸已經消失,我們終於看到了 Backend_Bound.Memory_Bound 瓶頸,在這種情況下,L1 Bound 和 L3 Bound所佔百分比幾乎相等,其餘的是核心執行。