硬體效能計數器(PMU)
/ PMU: pee-em-YOO /
假設你想知道的不只是某個迴圈「很慢」,而是「為什麼」——它在等記憶體、在分支預測失敗、還是單純做了很多工作?普通的計時無法回答。但 CPU 本身在執行時會替一些低階事件記下微小的計數,而你可以讀取它們。那些計數器就是硬體效能計數器,晶片上管理它們的單元叫效能監測單元(Performance Monitoring Unit),簡稱 PMU。
具體來說,PMU 有一小組實體計數器暫存器。你把每一個設定成計數某個特定的微架構事件——例如總週期數、退役指令數、某層快取未命中、分支預測失敗、或 TLB 未命中——接著每當該事件發生硬體就把那個暫存器加一,對你的程式幾乎沒有拖慢。軟體在區段的開頭與結尾讀取它們再相減。因為實體計數器只有幾個(常為 4 到 8 個),想一次計許多事件的工具會「多工」它們:先計事件 A 一陣子、再切到事件 B,然後把結果按比例放大,這在長時間執行下是準的,但會多一點雜訊。在 Linux 上標準介面是 perf_event_open() 系統呼叫,perf 工具與 PAPI 之類的函式庫都建立在它之上;同樣的計數器也能驅動取樣式剖析器,方法是每 N 個事件就觸發一次中斷(例如每一百萬次快取未命中取樣一次,看看未命中來自哪裡)。
它之所以重要,是因為計數器把「這很慢」變成一個診斷。看到高快取未命中率,把你指向記憶體佈局;高分支預測失敗率,把你指向不可預測的控制流;未命中很少卻每週期指令數很低,把你指向相依性或執行埠瓶頸。誠實的提醒:計數器事件的名稱與意義在不同 CPU 型號與廠商間各異,所以某顆晶片上的配方未必能搬到另一顆;讀取它們通常需要較高權限;而且一個原始計數只有在有基準對照時才有用——「一百萬次快取未命中」在你知道它是每次迴圈一次還是每千次一次之前,毫無意義。
$ perf stat ./myprog 12,400,103,221 cycles(週期) 8,201,556,090 instructions # 每週期 0.66 指令(IPC) 512,003,118 cache-misses # 約每 16 指令一次未命中 -> 受記憶體束縛
perf stat 讀取 PMU:週期、退役指令、推導出的 IPC,以及快取未命中——合起來就是一個受記憶體束縛迴圈的快速診斷。
計數器結果並非完全可重現:所謂「skid」指記錄到的指令可能落在真正觸發事件那一條之後幾條,多工會把估計值按比例放大,而看起來相同的事件在不同 CPU 上其實有別——把計數器當成有力證據,而非精確的絕對真相。