調(diào)優(yōu))
1. 先把核和核外面的東西分開一張架構(gòu)解剖圖很多人第一次跑性能分析時(shí)都會(huì)遇到同一個(gè)困惑任務(wù)管理器或者top顯示 CPU 利用率只有 40%但程序就是跑不快或者明明把線程數(shù)量調(diào)到了物理核數(shù)吞吐量卻卡在一個(gè)奇怪的數(shù)字上不動(dòng)了。我見過最多的誤判是把CPU 占用率直接等同于CPU 用滿了。實(shí)際上現(xiàn)代處理器的核心Core和核心之外的部分Uncore是兩套完全獨(dú)立的資源占用率只統(tǒng)計(jì)了前者而后者一旦成為瓶頸你在監(jiān)控面板上幾乎看不到任何異常。Core 負(fù)責(zé)執(zhí)行指令Uncore 負(fù)責(zé)把數(shù)據(jù)搬運(yùn)到 Core 手邊兩者的頻率、電壓域、功耗預(yù)算和性能歸屬都是分開的。搞不清這條分界線后面所有關(guān)于 P-Core、E-Core 的討論都是空中樓閣。這篇文章想聊的就是這條線。我會(huì)先拆開看 Core 和 Uncore 各自裝了什么再解釋 P-Core性能核和 E-Core能效核到底差在哪些微架構(gòu)細(xì)節(jié)上然后落到實(shí)操怎么用幾條命令把自己機(jī)器的大小核拓?fù)湔粘鰜碓趺唇壓嗽趺醋x uncore 的性能計(jì)數(shù)事件以及我在實(shí)際項(xiàng)目里踩過的幾個(gè)抖動(dòng)坑。內(nèi)容偏底層的性能工程方向但只要你會(huì)用命令行、跑過top就能跟著走下來。沒有懸空的理論每一條結(jié)論后面我都會(huì)說清楚它是從哪個(gè)計(jì)數(shù)器或者哪次壓測里來的。1.1 Core 里到底裝了什么一顆物理核的內(nèi)部可以粗糙地理解成前端 后端 私有緩存三段流水線。前端負(fù)責(zé)把指令從內(nèi)存里取出來經(jīng)過分支預(yù)測、譯碼變成微操作塞進(jìn)亂序窗口后端有整數(shù)單元、浮點(diǎn)單元、訪存單元按照數(shù)據(jù)依賴關(guān)系亂序執(zhí)行私有緩存是 L1 指令緩存、L1 數(shù)據(jù)緩存和每個(gè)核獨(dú)享的 L2 緩存。這三段全部是每個(gè)核一份互不共享所以核的數(shù)量直接決定了并行執(zhí)行的能力上限。你看到的物理核數(shù)說的就是這個(gè)獨(dú)立流水線的份數(shù)。關(guān)鍵點(diǎn)在于Core 的性能上限是被它的發(fā)射寬度和亂序窗口深度鎖死的。一顆寬度為 6 發(fā)射、亂序窗口能掛 500 多條微操作的核和一顆寬度為 3 發(fā)射、窗口只有一半的核在跑同一個(gè)單線程程序時(shí)IPC每周期執(zhí)行的指令數(shù)能差出一倍以上。這個(gè)差距跟頻率無關(guān)是純粹的結(jié)構(gòu)差異。所以你看到兩個(gè)核頻率差不多但跑分差很多的時(shí)候別急著懷疑散熱先去看它們的微架構(gòu)是不是同一個(gè)檔次。還有一層容易被忽略Core 內(nèi)部有自己的一套電源管理和時(shí)鐘門控。沒有任務(wù)的核會(huì)被深度降頻甚至部分?jǐn)嚯娺@就是為什么你在一臺(tái) 32 核的機(jī)器上跑一個(gè)單線程程序看到只有一個(gè)核在 3.5GHz 以上高頻運(yùn)行其他核顯示 800MHz。這不代表你的機(jī)器有問題反而說明調(diào)頻策略在正常工作。真正需要警惕的是相反的情況——所有核都顯示高頻但程序還是慢那基本可以確定瓶頸已經(jīng)跑到 Core 外面去了。1.2 Uncore 不是打雜的它是數(shù)據(jù)的高速公路Uncore 是個(gè)常被誤解的詞聽起來像是次要部分實(shí)際上它包含了所有核共享的關(guān)鍵基礎(chǔ)設(shè)施。按重要性排主要是這么幾塊末級(jí)緩存LLC也就是通常說的 L3、內(nèi)存控制器、核與核之間的互連總線環(huán)形或者網(wǎng)格狀拓?fù)?、?duì)外的高速接口控制器、集成顯示單元、系統(tǒng)代理以及統(tǒng)一管理整顆芯片頻率和功耗的電源管理單元。這些部件的共同特征是共享——不屬于任何一個(gè)核但所有核都要經(jīng)過它。為什么這個(gè)劃分值得單獨(dú)拿出來講因?yàn)樗念l率是獨(dú)立配置的。Core 頻率和 Uncore 頻率各有自己的倍頻在 BIOS 里通常是兩個(gè)分開的選項(xiàng)。很多主板默認(rèn)把 Uncore 頻率設(shè)成自動(dòng)在輕載時(shí)為了省電會(huì)把它壓得很低這時(shí)候 Core 還在高頻跑但每次訪問 LLC 未命中、每次讀內(nèi)存的延遲都被拉長了。表現(xiàn)出來就是主頻看著很高跑分卻沒提升游戲幀數(shù)還會(huì)莫名其妙地周期性掉一下。我做過一組對(duì)照測試同一臺(tái)機(jī)器同一顆芯片只改 Uncore 頻率限制。在內(nèi)存密集型負(fù)載下多線程隨機(jī)讀Uncore 從默認(rèn)的自動(dòng)檔拉到固定高頻吞吐量提升了接近 18%而 Core 頻率完全沒動(dòng)。這個(gè)數(shù)字放在單線程計(jì)算密集型負(fù)載上則幾乎為零。這就直接告訴你一件事判斷要不要?jiǎng)?Uncore先看你的負(fù)載是不是吃內(nèi)存帶寬或者緩存延遲。如果你的程序全部命中的是 L1/L2那 Uncore 怎么調(diào)都無所謂一旦開始大量走 LLC 和內(nèi)存它就變成第一優(yōu)先級(jí)。順帶說一個(gè)副產(chǎn)品內(nèi)存帶寬的理論上限其實(shí)是個(gè)算術(shù)題跟 Uncore 關(guān)系不大但很多人算錯(cuò)。以雙通道、4800MT/s、每通道 64 位為例上限是 2 × 4800 × 8 字節(jié) 76.8GB/s。但這是物理層的理論值實(shí)際能跑到 65%75% 就不錯(cuò)了剩下的損耗來自協(xié)議開銷、刷新周期和 Uncore 側(cè)的調(diào)度效率。所以你在壓測里看到 50GB/s 就以為被限制了很可能只是正常損耗而不是哪里沒配對(duì)。1.3 為什么這條分界線決定了你后面所有的調(diào)優(yōu)動(dòng)作把 Core 和 Uncore 分開之后很多玄學(xué)現(xiàn)象就有了統(tǒng)一的解釋框架。舉個(gè)例子一個(gè)延遲敏感的在線服務(wù)P99 延遲時(shí)不時(shí)飆到幾十毫秒CPU 利用率很平穩(wěn)。如果你只盯著 Core會(huì)得出CPU 沒問題的結(jié)論然后開始懷疑網(wǎng)絡(luò)、懷疑鎖爭用。但如果看一眼 Uncore 側(cè)的內(nèi)存控制器計(jì)數(shù)器和 LLC 未命中率很可能發(fā)現(xiàn)是另一條業(yè)務(wù)線的批量任務(wù)在周期性地沖刷 LLC把你的熱數(shù)據(jù)全擠出去了。這時(shí)候的修復(fù)方案不是加核而是做緩存隔離或者錯(cuò)峰調(diào)度。再比如虛擬化場景。很多虛擬化平臺(tái)把每個(gè)虛擬 CPU 映射到一個(gè)物理核但 Uncore 是宿主機(jī)層面共享的客戶機(jī)里面完全看不到。結(jié)果就是客戶機(jī)的性能隨鄰居的負(fù)載變化而波動(dòng)而你在客戶機(jī)內(nèi)部查任何指標(biāo)都是正常的。這類問題不搞清楚 Core 和 Uncore 的分層排查方向會(huì)一直跑偏。所以我后面講 P-Core 和 E-Core 的時(shí)候會(huì)一直用這個(gè)框架Core 層面比的是單核執(zhí)行效率Uncore 層面比的是共享資源供給能力。你遇到任何性能問題第一件事就是判斷它歸到哪一層答案通常在五分鐘內(nèi)就能定下來。2. P-Core 與 E-Core不是快慢之分而是面積-延遲-吞吐的三角取舍大小核這個(gè)詞被用了很多年但真正理解的人不多。最常見的誤解是P 核就是大核E 核就是小核小核是縮水版。這個(gè)描述不算錯(cuò)但它掩蓋了設(shè)計(jì)動(dòng)機(jī)。E-Core 從來不是做不好的 P-Core它是為了另一個(gè)目標(biāo)函數(shù)優(yōu)化出來的產(chǎn)物在同樣的硅片面積和功耗預(yù)算下最大化多線程吞吐。這兩個(gè)目標(biāo)在微架構(gòu)層面是沖突的所以必須做成兩種核。理解了這個(gè)沖突你才能明白為什么調(diào)度器這么難寫以及為什么有些任務(wù)綁到 E 核上反而更穩(wěn)。2.1 兩種核心的微架構(gòu)差異攤開看把兩種核放在一張表里對(duì)比差異會(huì)非常直觀。下面這些數(shù)字不是某一款具體產(chǎn)品的參數(shù)而是這類混合架構(gòu)的典型量級(jí)你手上的機(jī)器可能略有出入但比例關(guān)系是穩(wěn)定的維度P-Core性能核E-Core能效核發(fā)射寬度寬6 左右窄3 左右亂序窗口深500 微操作淺200 左右同時(shí)多線程支持一個(gè)核跑兩個(gè)邏輯線程通常不支持私有 L2每核獨(dú)享容量大多核成簇共享單核可見容量小峰值頻率高明顯低一檔單核面積基準(zhǔn) 1 份約為 P 核的 1/4分支預(yù)測器大容量、多級(jí)精簡版寬向量指令支持部分型號(hào)不支持或降速執(zhí)行表格里最值得盯的是亂序窗口和同時(shí)多線程這兩行。亂序窗口決定了處理器能在多大范圍內(nèi)重排指令來掩蓋延遲窗口越深遇到緩存未命中和長延遲操作時(shí)越從容。這就是為什么同樣跑一個(gè)分支密集、隨機(jī)訪存的程序P 核的 IPC 能做到 E 核的兩倍以上。同時(shí)多線程的意義在于當(dāng)一個(gè)線程因?yàn)榈却龜?shù)據(jù)而停頓時(shí)同一個(gè)核上的第二個(gè)線程可以填補(bǔ)執(zhí)行空隙把流水線利用率從 40% 拉到 70%。E 核沒有這個(gè)機(jī)制它靠的是數(shù)量——單個(gè)效率低但占地小可以堆很多個(gè)。還有一個(gè)經(jīng)常被忽略的差異E 核通常是成簇共享 L2的。四個(gè) E 核共享一塊 L2意味著它們之間的數(shù)據(jù)交換比跨簇快得多但同時(shí)也意味著緩存容量是按簇計(jì)算的不是按核。如果你的程序在同一簇的 E 核之間頻繁共享數(shù)據(jù)性能會(huì)比分散綁到不同簇上明顯更好。這一點(diǎn)在做線程親和性設(shè)計(jì)時(shí)非常有用后面第三章會(huì)具體講怎么看出來。2.2 面積經(jīng)濟(jì)學(xué)為什么廠商愿意用一個(gè)大核的地盤換四個(gè)小核一個(gè) P 核的硅片面積大致能做四到五個(gè) E 核這個(gè)比例是整個(gè)混合架構(gòu)的經(jīng)濟(jì)基礎(chǔ)。假設(shè)你有固定的面積預(yù)算方案 A 是全做 P 核你能拿到 8 個(gè)核 16 個(gè)線程方案 B 是 4 個(gè) P 核加 16 個(gè) E 核總共 20 個(gè)核 24 個(gè)線程。單線程性能方案 A 略強(qiáng)因?yàn)?P 核可以跑到更高頻率熱密度也更集中但多線程吞吐方案 B 能拉開 30% 以上的差距因?yàn)榫€程數(shù)接近翻倍。這就解釋了為什么廠商愿意承擔(dān)調(diào)度復(fù)雜度也要做混合架構(gòu)在消費(fèi)級(jí)場景里絕大多數(shù)日常負(fù)載的線程數(shù)遠(yuǎn)超 8 個(gè)而真正時(shí)長超過幾秒的計(jì)算密集單線程任務(wù)反而很少。用面積換吞吐在統(tǒng)計(jì)意義上幾乎總是劃算的。代價(jià)是軟件側(cè)要處理異構(gòu)性這筆賬被轉(zhuǎn)嫁給了操作系統(tǒng)和開發(fā)者。但這里有個(gè)反直覺的結(jié)論我在實(shí)際測試?yán)矧?yàn)證過很多次E 核的能效優(yōu)勢不是絕對(duì)的。它的單位功耗性能確實(shí)更高但因?yàn)樗粋€(gè)任務(wù)跑完需要更長時(shí)間總能耗未必更低。只有當(dāng)任務(wù)是多線程并行、能把所有核都喂飽的時(shí)候E 核的能效優(yōu)勢才明顯體現(xiàn)出來。如果你把一個(gè)固定工作量的任務(wù)放到 E 核上跑它可能用更低的功率跑了兩倍的時(shí)間總電量反而更多。所以能效核這個(gè)名字應(yīng)該理解成單位面積單位功耗的吞吐效率高不是更省電。這個(gè)認(rèn)知直接影響調(diào)優(yōu)策略。我看到不少人為了省電主動(dòng)把后臺(tái)服務(wù)全部綁到 E 核上結(jié)果這些服務(wù)因?yàn)檠舆t變高、超時(shí)重試增多整體功耗反而上升了。正確的做法是延遲敏感的服務(wù)留在 P 核批處理、日志壓縮、索引重建這類吞吐型任務(wù)丟給 E 核。這樣兩類任務(wù)的資源需求剛好互補(bǔ)也讓調(diào)度器的決策變得簡單。2.3 混合架構(gòu)真正的難題調(diào)度器怎么知道該把線程放哪硬件層面有一層機(jī)制在解決這個(gè)問題處理器會(huì)實(shí)時(shí)采集每個(gè)核的負(fù)載特征通過一個(gè)小型的狀態(tài)表告訴操作系統(tǒng)這個(gè)線程適合放在哪類核上。它評(píng)估的東西包括線程占用多少資源、是否在用寬向量指令、是否頻繁等待內(nèi)存、以及核心當(dāng)前的熱狀態(tài)。操作系統(tǒng)拿到這些提示后再疊加自己的調(diào)度策略做最終決定。這層協(xié)作機(jī)制是整個(gè)混合架構(gòu)能否跑好的關(guān)鍵也是各家實(shí)現(xiàn)差異最大的地方。理解它的工作方式對(duì)你排查問題很有幫助。舉個(gè)真實(shí)的例子我在一臺(tái)混合架構(gòu)機(jī)器上編譯一個(gè)大項(xiàng)目用make -j全核并行。理論上應(yīng)該比純 P 核機(jī)器快但實(shí)測慢了 15%。查下來原因在于編譯器在某些源文件上會(huì)使用寬向量指令而這些線程一旦被遷到不支持該指令集的小核上就要走軟件的兼容路徑速度掉一大截。操作系統(tǒng)雖然有補(bǔ)償機(jī)制會(huì)盡量避免把這類線程遷走但在負(fù)載劇烈波動(dòng)時(shí)還是會(huì)發(fā)生。解決方案很簡單把編譯任務(wù)限制在 P 核上跑。做完之后不僅速度快了波動(dòng)也消失了。另一個(gè)常見癥狀是頻率不一致導(dǎo)致的時(shí)間測量失真。你在兩個(gè)線程里各跑一遍同樣的計(jì)算一個(gè)落在 P 核上花了 100 毫秒另一個(gè)落在 E 核上花了 180 毫秒如果你的程序用誰先完成來做邏輯判斷就會(huì)產(chǎn)生完全不可復(fù)現(xiàn)的行為。這類 bug 極難定位因?yàn)樗诩兺瑯?gòu)機(jī)器上永遠(yuǎn)不會(huì)出現(xiàn)。我的建議是凡是依賴執(zhí)行速度做判斷的邏輯比如超時(shí)閾值、競爭選舉、負(fù)載均衡策略都必須顯式地在代碼里做歸一化或者干脆用邏輯時(shí)鐘替代物理時(shí)鐘。最麻煩的場景是虛擬化。虛擬機(jī)里通??床坏胶诵念愋偷膮^(qū)分宿主機(jī)的調(diào)度器把 vCPU 映射到哪種核上客戶機(jī)完全不知情。這就造成了一個(gè)典型的性能謎題同樣的鏡像、同樣的配置兩臺(tái)宿主機(jī)上的性能差 20%。要解決只能在宿主機(jī)層面做綁定把關(guān)鍵虛擬機(jī)的 vCPU 釘在 P 核上或者至少保證同一個(gè)虛擬機(jī)的 vCPU 落在同類核上。3. 把拓?fù)湔粘鰜硭念惷羁焖倜遄约旱臋C(jī)器理論講完了落到手上。每次接手一臺(tái)新機(jī)器我做的第一件事永遠(yuǎn)是摸拓?fù)湟驗(yàn)檫@個(gè)動(dòng)作成本極低但能避免后面一大堆誤判。下面這幾條命令覆蓋了從有哪些核到Uncore 在干什么的全部信息順序建議照著我寫的來因?yàn)樾畔⑹侵饘由钊氲摹?.1 一分鐘看清核心布局第一條命令是lscpu輸出里會(huì)直接列出架構(gòu)、物理核數(shù)、邏輯核數(shù)、NUMA 節(jié)點(diǎn)數(shù)和緩存層級(jí)?;旌霞軜?gòu)的機(jī)器上你會(huì)看到每核線程數(shù)這一項(xiàng)不統(tǒng)一——如果它顯示 1同時(shí)核數(shù)等于線程數(shù)那很可能全是 E 核或者關(guān)了超線程如果物理核數(shù)和邏輯核數(shù)的比值不是整數(shù)倍關(guān)系那基本可以確認(rèn)是混合架構(gòu)。接著用擴(kuò)展模式拿到更細(xì)的信息lscpu -eCORE,SOCKET,NODE,MAXMHZ,MINMHZ,L1D:L1I:L2:L3,ONLINE這條命令的輸出是逐邏輯 CPU 一行的表格。判斷哪些是 P 核最直接的方式是看MAXMHZ列——P 核的最高頻率通常比 E 核高出 40% 以上一眼就能分出來。另一個(gè)線索是CORE列如果兩個(gè)邏輯 CPU 共享同一個(gè) CORE 編號(hào)說明它們是一對(duì)超線程兄弟而這種情況只會(huì)出現(xiàn)在 P 核上。如果你想要更圖形化的呈現(xiàn)用lstopo-no-graphics屬于 hwloc 工具集。它會(huì)把 NUMA 節(jié)點(diǎn)、緩存層級(jí)、每個(gè)核的兄弟關(guān)系畫成樹狀結(jié)構(gòu)還能顯示每個(gè)核到內(nèi)存控制器的距離關(guān)系。我在做跨節(jié)點(diǎn)性能分析時(shí)基本離不開它因?yàn)橹挥羞@張圖能直觀地告訴你這兩個(gè)核共享什么、不共享什么。再補(bǔ)一條常被忽略的查看超線程兄弟對(duì)的具體編號(hào)。for i in $(seq 0 $(($(nproc)-1))); do echo cpu$i - $(cat /sys/devices/system/cpu/cpu$i/topology/thread_siblings_list) done輸出里每一對(duì)相同的數(shù)字就是一對(duì)兄弟。做延遲敏感型綁核時(shí)這個(gè)信息決定成敗因?yàn)榘褍蓚€(gè)高負(fù)載線程放到一對(duì)兄弟上它們會(huì)爭搶同一個(gè)核的執(zhí)行單元實(shí)測性能損失能到 30%。更隱蔽的是有些機(jī)器的編號(hào)排列不是0-1 是兄弟而是0-8 是兄弟你憑直覺寫taskset -c 0-1恰好就把兩個(gè)線程塞進(jìn)了同一個(gè)核。3.2 從 sysfs 里讀出 Uncore 與頻率域/sys/devices/system/cpu/這個(gè)目錄是臺(tái)金礦。幾個(gè)必看的位置# 每顆核支持哪些頻率檔位 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies # 當(dāng)前的調(diào)頻策略powersave / performance / schedutil 等 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 緩存共享關(guān)系判斷哪些核共享 L2 cat /sys/devices/system/cpu/cpu0/cache/index2/shared_cpu_list最后一條特別有用?;旌霞軜?gòu)上P 核的 L2 是每核獨(dú)享的shared_cpu_list只有自己而 E 核成簇共享 L2shared_cpu_list會(huì)列出同簇的所有邏輯 CPU。這樣你不用猜直接就能讀出 E 核的簇劃分。我在做線程池親和性設(shè)計(jì)時(shí)會(huì)直接把同一簇的 E 核作為一組讓組內(nèi)的線程共享數(shù)據(jù)結(jié)構(gòu)跨組只做粗粒度通信這個(gè)改動(dòng)在一個(gè)數(shù)據(jù)聚合服務(wù)上帶來了約 12% 的吞吐提升。Uncore 頻率的讀取在較新的內(nèi)核上有專門的 sysfs 節(jié)點(diǎn)路徑包含uncore_frequency字樣里面通常有min_freq_khz、max_freq_khz、current_freq_khz這幾個(gè)文件??梢灾苯訉憁ax_freq_khz來鎖定上限這是免進(jìn) BIOS 就能調(diào) Uncore 的最省事的方式。實(shí)時(shí)監(jiān)控我推薦turbostat它輸出的表格里有Bzy_MHz核心實(shí)際頻率、UncMHzUncore 實(shí)際頻率、PkgWatt整包功耗這幾列。盯著Bzy_MHz和UncMHz的比值看就能判斷當(dāng)前負(fù)載是卡在核心上還是卡在 Uncore 上。經(jīng)驗(yàn)值是這樣的如果Bzy_MHz已經(jīng)跑到接近規(guī)格上限而UncMHz穩(wěn)定在額定值說明是算力瓶頸如果Bzy_MHz忽高忽低、UncMHz也有明顯波動(dòng)那大概率是共享資源在搶。3.3 PMU 事件把 Core 和 Uncore 的賬分開算性能計(jì)數(shù)器是分清兩層界限的終極手段?,F(xiàn)代處理器的 PMU 分為兩部分核心內(nèi)的計(jì)數(shù)器用來數(shù)周期、指令、分支預(yù)測失敗、L1/L2 訪問Uncore 的計(jì)數(shù)器用來數(shù)內(nèi)存控制器吞吐、LLC 命中與缺失、互連總線流量。在perf里這兩套事件的命名前綴不一樣以u(píng)ncore_開頭的都屬于 Uncore 側(cè)。先看有哪些可用的perf list | grep -i uncore你會(huì)看到諸如內(nèi)存控制器的讀帶寬、寫帶寬、LLC 查找次數(shù)、跨片互連流量這類事件。數(shù)量不多但每一個(gè)都直指共享資源的供給能力。接下來的典型用法# 核心側(cè)IPC 和緩存效率 perf stat -e cycles,instructions,branches,branch-misses,L1-dcache-load-misses,LLC-load-misses -p pid # Uncore 側(cè)內(nèi)存帶寬事件名視平臺(tái)而定照 perf list 的輸出替換 perf stat -e uncore_imc_0/cas_count_read/,uncore_imc_0/cas_count_write/ -a sleep 5第一次跑的時(shí)候要注意兩個(gè)坑。第一Uncore 事件通常是整包級(jí)別的需要-a全系統(tǒng)模式不能只看單個(gè)進(jìn)程因?yàn)樗y(tǒng)計(jì)的是共享硬件的行為沒法歸因到某個(gè)進(jìn)程上。第二在虛擬機(jī)里這些計(jì)數(shù)器絕大多數(shù)是禁用的你看到event not supported是正常的別浪費(fèi)時(shí)間排查。有了這兩組數(shù)據(jù)判斷路徑就很清楚了。用下面這張表做個(gè)快速索引觀測到的現(xiàn)象判斷方向下一步動(dòng)作IPC 低L1 未命中率高算法訪存模式差優(yōu)化數(shù)據(jù)結(jié)構(gòu)提高局部性IPC 正常LLC 未命中率高工作集超過 LLC分塊處理或做緩存隔離LLC 未命中率正常內(nèi)存帶寬接近上限Uncore 供給不足提 Uncore 頻率、加通道、改 NUMA 策略指令數(shù)突增但周期數(shù)沒變走了兼容路徑檢查是否被調(diào)度到不支持某指令集的核上核心利用率低Uncore 流量高數(shù)據(jù)搬運(yùn)開銷大減少跨節(jié)點(diǎn)訪問檢查互連流量我排查過一個(gè)程序變慢但 CPU 不滿的案例最后就是靠第三行定位的LLC 未命中率在正常范圍但內(nèi)存控制器的讀帶寬已經(jīng)打到了理論上限的 92%。也就是說 Core 本身效率沒問題是 Uncore 喂不飽這么多個(gè) Core。這種時(shí)候唯一有效的方案是把工作集縮小到能放進(jìn) LLC 的尺寸或者干脆減少并發(fā)核數(shù)——聽起來反直覺但并發(fā)變少后每個(gè)線程拿到的帶寬更多總完成時(shí)間反而縮短了。4. 綁核不是萬靈藥把線程放到對(duì)的位置上綁核這個(gè)動(dòng)作被用錯(cuò)的頻率遠(yuǎn)高于被用對(duì)。我看到過太多人一遇到性能抖動(dòng)就加taskset結(jié)果把本來是負(fù)載不均衡的問題變成了資源閑置。綁核的本質(zhì)是控制變量你放棄了調(diào)度器動(dòng)態(tài)調(diào)整的自由度換取確定性。所以只有在確定調(diào)劑動(dòng)態(tài)性是干擾源的時(shí)候綁核才是正解。判斷標(biāo)準(zhǔn)很簡單如果你的問題是數(shù)值不可復(fù)現(xiàn)綁核有用如果問題是整體不夠快綁核通常沒用甚至有害。4.1 延遲敏感型任務(wù)的標(biāo)準(zhǔn)姿勢在線服務(wù)、交易系統(tǒng)、實(shí)時(shí)數(shù)據(jù)處理這類任務(wù)的核心指標(biāo)是 P99 而不是平均值。它們最怕的東西是意外——意外的遷移、意外的搶占、意外的緩存失效。針對(duì)這類負(fù)載我用的標(biāo)準(zhǔn)配置是這樣的# 1. 選出 P 核避開超線程兄弟對(duì) # 假設(shè) P 核是 cpu 0-15兄弟對(duì)為 (0,8) (1,9) ... # 只取每對(duì)的第一個(gè)得到 0-7 # 2. 綁核 綁內(nèi)存節(jié)點(diǎn) taskset -c 0-7 numactl --membind0 ./service # 3. 把中斷挪到 E 核上避免打斷關(guān)鍵線程 echo 16-23 /proc/irq/default_smp_affinity_list這三步的順序很重要我分開解釋為什么。第一步只取每對(duì)超線程的第一個(gè)是因?yàn)橥粚?duì)兄弟共享執(zhí)行單元和 L1/L2讓兩個(gè)高負(fù)載線程擠在一對(duì)兄弟上效率損失比多用一個(gè)物理核大得多。第二步把內(nèi)存綁定到本地節(jié)點(diǎn)是為了避免跨節(jié)點(diǎn)訪問——跨節(jié)點(diǎn)的一次內(nèi)存訪問延遲能從 80 納秒漲到 140 納秒以上對(duì)延遲敏感型任務(wù)這是致命的。第三步把中斷趕走是因?yàn)橐粋€(gè)中斷處理看起來只有幾微秒但它會(huì)污染目標(biāo)核的 L1/L2讓后面幾百微秒內(nèi)的緩存全部失效。第三步的收益經(jīng)常被嚴(yán)重低估。我在一個(gè)消息處理服務(wù)上做過對(duì)照其他條件不變只把網(wǎng)絡(luò)中斷從業(yè)務(wù)線程所在的核上挪走P99 延遲從 4.2 毫秒降到 1.8 毫秒降幅超過一半。原因就是中斷處理會(huì)沖刷緩存而緩存預(yù)熱需要時(shí)間。這個(gè)改動(dòng)的成本幾乎為零收益卻非常可觀我建議所有延遲敏感型服務(wù)都檢查一遍。還有一個(gè)進(jìn)階手段用 cgroup 的 cpuset 做隔離比taskset更徹底因?yàn)樗芊乐蛊渌M(jìn)程被調(diào)度到這些核上。新版 cgroup 里可以給 cpuset 設(shè)置獨(dú)占分區(qū)效果類似于內(nèi)核啟動(dòng)參數(shù)里的核隔離但不需要重啟。容器化部署時(shí)這個(gè)方案特別合適因?yàn)楦綦x策略可以跟著容器一起聲明。4.2 吞吐型任務(wù)別急著關(guān) E-Core另一個(gè)極端是把 E 核全部關(guān)掉。這個(gè)做法的動(dòng)機(jī)可以理解——消除異構(gòu)性讓性能可預(yù)測。但我實(shí)測下來在吞吐型負(fù)載上關(guān)掉 E 核基本是凈虧損。做過一組對(duì)比一個(gè)日志分析和聚合的批處理任務(wù)在全核含 E 核上跑用 42 秒關(guān)掉 E 核只用 P 核跑用 51 秒。也就是說那些慢核貢獻(xiàn)了 18% 的吞吐白扔掉很可惜。正確的做法不是關(guān)核而是讓任務(wù)匹配核的特性。我給這類任務(wù)設(shè)計(jì)線程池時(shí)的策略是這樣的主控線程和 I/O 線程放在 P 核保證響應(yīng)速度工作線程按批次分發(fā)給 E 核因?yàn)樗鼈冏龅氖羌冇?jì)算、無依賴、可并行的活。這樣既拿到了 E 核的數(shù)量優(yōu)勢又不會(huì)因?yàn)檎{(diào)度錯(cuò)位而拖慢關(guān)鍵路徑。具體實(shí)現(xiàn)上可以用兩個(gè)獨(dú)立的任務(wù)隊(duì)列配合不同的親和性設(shè)置# 主控 IO 線程池 taskset -c 0-3 ./app --io-threads4 # 計(jì)算線程池綁到 E 核簇上假設(shè) E 核是 16-23 taskset -c 16-23 ./worker --concurrency8這里有個(gè)細(xì)節(jié)E 核的簇劃分要和線程數(shù)對(duì)齊。如果 E 核是 4 核一簇、共兩簇那綁 8 個(gè)線程是合適的如果你綁 6 個(gè)就可能出現(xiàn)某一簇被塞了 4 個(gè)、另一簇只有 2 個(gè)的不均衡情況實(shí)際性能比綁 4 個(gè)還差。這個(gè)坑我踩過一次線程數(shù)從 8 改成 6 之后吞吐掉了 20%查了很久才發(fā)現(xiàn)是簇內(nèi)競爭導(dǎo)致的。還有一類任務(wù)特別適合 E 核長時(shí)間運(yùn)行但不追求單次速度的后臺(tái)任務(wù)比如日志輪轉(zhuǎn)、索引合并、緩存預(yù)熱、健康檢查。這些任務(wù)的特點(diǎn)是優(yōu)先級(jí)低、可以容忍延遲、但數(shù)量多。把它們綁到 E 核上既釋放了 P 核又避免了它們被調(diào)度器到處搬來搬去打擾關(guān)鍵業(yè)務(wù)。4.3 中斷、容器與虛擬化里的三個(gè)坑第一個(gè)坑是容器的 CPU 限制和親和性沖突。給容器設(shè)了--cpus4之后運(yùn)行時(shí)會(huì)在宿主機(jī)層面做帶寬限制但如果你同時(shí)又在容器內(nèi)用taskset綁了 4 個(gè)核而這 4 個(gè)核恰好跨了 NUMA 節(jié)點(diǎn)那你既失去了調(diào)度的靈活性又引入了跨節(jié)點(diǎn)訪問。我建議容器場景只保留一套約束要么用 cpuset 做硬綁定同時(shí)解決配額和親和性要么只做配額限制讓調(diào)度器自由發(fā)揮。兩套機(jī)制疊加是最容易出問題的組合。第二個(gè)坑是虛擬機(jī)的 vCPU 與物理核錯(cuò)配。有些平臺(tái)默認(rèn)把 vCPU 均勻打散到所有物理核上這在混合架構(gòu)下會(huì)導(dǎo)致同一個(gè)虛擬機(jī)的 vCPU 有的在 P 核有的在 E 核客戶機(jī)內(nèi)部看到的同構(gòu)多核其實(shí)是假的。表現(xiàn)是客戶機(jī)里的并行程序性能波動(dòng)極大而且無法通過客戶機(jī)內(nèi)部的任何手段解決。方案是在宿主機(jī)層面配置親和性規(guī)則保證同一個(gè)虛擬機(jī)的 vCPU 落在同類核上。這個(gè)改動(dòng)在編排層面做一次就行屬于一勞永逸的收益。第三個(gè)坑是中斷親和性與綁核的隱性沖突。你在業(yè)務(wù)線程上做了綁核但中斷控制器仍然可能把中斷送到這些核上尤其是當(dāng)你只綁了幾個(gè)核而系統(tǒng)有幾十個(gè)核的時(shí)候中斷分布會(huì)很不均勻。檢查方式是看/proc/interrupts里每個(gè)核的中斷計(jì)數(shù)如果發(fā)現(xiàn)某個(gè)核的計(jì)數(shù)遠(yuǎn)高于其他核那就是沒配對(duì)。修復(fù)方式是把中斷親和性顯式設(shè)置為不包含業(yè)務(wù)核的那組 CPU。這個(gè)動(dòng)作在壓測前做一次能消除相當(dāng)一部分的延遲毛刺。5. 三個(gè)真實(shí)抖動(dòng)案例的排查鏈路光講原理容易飄我挑三個(gè)自己實(shí)際處理過的案例把完整的排查路徑攤開。這三個(gè)案例的共同點(diǎn)是表面癥狀相似都是變慢但根因分別落在 Core 層、Uncore 層和調(diào)度層處理方式完全不同。我特意保留中間走過的彎路因?yàn)榕挪檫^程本身就是最有價(jià)值的部分。5.1 案例一單線程程序越跑越慢現(xiàn)象一個(gè)計(jì)算密集的單線程任務(wù)跑第一次用 30 秒第二次 33 秒第三 次 37 秒重啟進(jìn)程后又回到 30 秒。這個(gè)模式非常反常因?yàn)榧冇?jì)算任務(wù)不應(yīng)該有累積效應(yīng)。第一反應(yīng)是內(nèi)存泄漏或者 GC 壓力但監(jiān)控顯示內(nèi)存平穩(wěn)。第二步懷疑降頻用turbostat盯著看果然發(fā)現(xiàn)了問題任務(wù)運(yùn)行 30 秒后核心頻率從 4.2GHz 掉到 3.4GHzPkgWatt也接近了功耗上限。也就是說散熱或者功耗墻在起作用。但故事沒結(jié)束。如果只是撞功耗墻為什么重啟進(jìn)程就好了繼續(xù)往下查發(fā)現(xiàn)是另一個(gè)原因疊加這個(gè)程序在運(yùn)行過程中不斷分配新內(nèi)存雖然邏輯上釋放了但分配器沒有歸還給操作系統(tǒng)導(dǎo)致工作集持續(xù)增長LLC 命中率從 85% 一路掉到 40%。頻率下降和緩存失效兩個(gè)因素疊加才造成了這個(gè)線性變慢的曲線。修法分兩步。針對(duì)功耗墻這屬于硬件限制能做的只有改善散熱或者降低期望針對(duì)緩存失效把內(nèi)存分配器換成了會(huì)主動(dòng)歸還內(nèi)存的實(shí)現(xiàn)并且在批處理邊界上顯式觸發(fā)一次整理。改完之后曲線變平了穩(wěn)定在 31 秒左右。這個(gè)案例的教訓(xùn)是癥狀是單一的原因往往是復(fù)合的查到第一個(gè)原因就停下很容易做出錯(cuò)誤的歸因。5.2 案例二內(nèi)存帶寬上不去CPU 利用率卻不滿現(xiàn)象一個(gè)多線程數(shù)據(jù)處理任務(wù)理論帶寬需求約 60GB/s實(shí)測只跑到 38GB/s同時(shí) CPU 利用率卡在 55% 上不去。加線程數(shù)沒用提高到物理核數(shù)的兩倍還是一樣。這次的排查路徑走的是 Uncore 側(cè)。先算理論值雙通道 4800MT/s上限 76.8GB/s實(shí)測 38GB/s 只有一半明顯不正常。然后用perf看 Uncore 事件發(fā)現(xiàn)內(nèi)存控制器的讀帶寬計(jì)數(shù)和實(shí)際業(yè)務(wù)量對(duì)得上說明請求確實(shí)發(fā)出去了。那問題就在請求的路上。下一步看 NUMA 分布用numactl --hardware和每個(gè)線程的本地節(jié)點(diǎn)命中率。發(fā)現(xiàn)有一半的線程分配的內(nèi)存落在了遠(yuǎn)端節(jié)點(diǎn)上因?yàn)榉峙洳呗允悄J(rèn)的本地優(yōu)先但進(jìn)程啟動(dòng)時(shí)所在的核和后來實(shí)際運(yùn)行的核不在同一個(gè)節(jié)點(diǎn)。遠(yuǎn)端訪問會(huì)走互連總線帶寬受限于互連而不是內(nèi)存控制器。修法就是顯式綁定numactl --cpunodebind0 --membind0 ./app或者更細(xì)粒度地用--interleave交錯(cuò)分配讓每個(gè)線程的訪問都有一部分在本地。改完之后帶寬從 38GB/s 漲到 62GB/sCPU 利用率也跟著上到了 88%。這個(gè)案例說明一件事帶寬上不去不一定是物理限制很可能是流量走錯(cuò)了路。而流量路徑這個(gè)問題在 Core 層面完全看不出來。5.3 案例三壓測數(shù)字忽高忽低復(fù)現(xiàn)不了現(xiàn)象同一個(gè)壓測腳本連續(xù)跑十次吞吐量在 8000 到 12000 QPS 之間隨機(jī)波動(dòng)沒有任何規(guī)律。這種問題最讓人頭疼因?yàn)樗馕吨銦o法驗(yàn)證任何優(yōu)化是否有效。排查的第一步是消除變量。先把線程數(shù)固定、綁核、綁 NUMA跑十次波動(dòng)范圍收窄到 9000-11000但沒消失。說明還有別的變量。繼續(xù)查用turbostat記錄每次運(yùn)行時(shí)的頻率曲線發(fā)現(xiàn)高頻運(yùn)行的那幾次Bzy_MHz普遍更高而且波動(dòng)更小。這就把矛頭指向了調(diào)頻策略。再往下看問題出在調(diào)頻策略的響應(yīng)速度上。默認(rèn)的策略是按需調(diào)頻它根據(jù)最近的負(fù)載歷史決定頻率。壓測啟動(dòng)的瞬間負(fù)載突然拉滿但策略需要幾百毫秒才能把頻率拉上去如果這期間恰好有另一個(gè)短暫的空閑窗口頻率又會(huì)被拉下來。每次運(yùn)行的時(shí)機(jī)不同落在這個(gè)上升沿的位置就不同于是數(shù)字隨機(jī)波動(dòng)。修復(fù)方式很直接把調(diào)頻策略切成固定性能模式。cpupower frequency-set -g performance改完之后波動(dòng)范圍收窄到 9500-10200 QPS可以接受。代價(jià)是待機(jī)功耗上升但對(duì)壓測環(huán)境來說這個(gè)代價(jià)可以忽略。這個(gè)案例我印象很深因?yàn)樗虝?huì)我一件事在混合架構(gòu)上做性能測試第一件事是把所有能自動(dòng)變的東西固定住。頻率、綁核、NUMA、中斷親和性全部固定之后你才能開始測真正想測的東西。否則你測到的只是各路自動(dòng)策略的隨機(jī)組合。后來我把這套流程固化成了一個(gè)測試前的檢查腳本每次壓測前先跑一遍把不確定因素全部消除。這個(gè)習(xí)慣讓我的測試數(shù)據(jù)可信度提升了一個(gè)檔次也避免了很多改了半天沒效果的無效工作。6. 一份可以直接抄的調(diào)優(yōu)清單與幾個(gè)反直覺結(jié)論寫到這里把可操作的東西收斂成一份清單你照著檢查一遍就行不用每次都從頭推。第一步永遠(yuǎn)是定位分層。跑lscpu和turbostat看Bzy_MHz和UncMHzIPC 高不高LLC 未命中率高不高。這一步?jīng)Q定后面往哪個(gè)方向使勁。第二步摸拓?fù)溆胠scpu -e和thread_siblings_list把 P 核、E 核、超線程兄弟對(duì)、E 核簇的劃分全部列出來寫在紙上。第三步改配置按負(fù)載類型選方案延遲敏感就綁 P 核、避開兄弟對(duì)、綁本地節(jié)點(diǎn)、挪走中斷吞吐型就全核開跑、主控放 P 核、工作線程按簇綁 E 核。第四步驗(yàn)證改一項(xiàng)測一項(xiàng)別一次改五個(gè)地方然后不知道是誰起的作用。幾個(gè)我認(rèn)為最值得記住的反直覺結(jié)論。E 核不一定更省電只有在多線程滿載時(shí)才體現(xiàn)能效優(yōu)勢單任務(wù)放上去反而更耗電。Uncore 頻率的影響遠(yuǎn)大于多數(shù)人的預(yù)期內(nèi)存密集型負(fù)載下調(diào)整它能帶來十幾個(gè)百分點(diǎn)的差異而 Core 頻率往往已經(jīng)撞墻了。綁核是控制變量的手段不是提速的手段用來消除波動(dòng)有效用來提升峰值通常無效。IPC 是比頻率更可靠的性能指標(biāo)頻率可以做得很漂亮但 IPC 騙不了人。最后分享一個(gè)我一直在用的小技巧給機(jī)器建立一份性能基線檔案。在干凈的、空載的狀態(tài)下跑一遍標(biāo)準(zhǔn)負(fù)載可以是固定的編譯任務(wù)、固定的數(shù)據(jù)處理腳本記錄下耗時(shí)、IPC、內(nèi)存帶寬、各層緩存命中率。以后任何一次性能異常都拿這份基線做對(duì)比能在一分鐘內(nèi)判斷出是哪一層退化了。這份檔案我建議跟著機(jī)器走而不是跟著人走因?yàn)榛€的價(jià)值恰恰在于它的穩(wěn)定性和可比性。真正把 Core 和 Uncore 分開看之后我最大的收獲不是學(xué)會(huì)了調(diào)幾個(gè)參數(shù)而是形成了一個(gè)條件反射看到任何性能問題先問這是執(zhí)行的問題還是供給的問題。這個(gè)二分法幾乎適用于所有場景包括那些看起來跟 CPU 完全無關(guān)的場景。想清楚這一層剩下的都是查文檔和試參數(shù)的體力活。