一顆出廠原封不動的 AMD Ryzen 7 5800H,執行同一行 fxrstor64,耗時六十二秒——198,002,498,236 個週期,只因為隔壁幾顆核心把同一段 PCIe 線路灌到動彈不得。
一道指令,跑六十二秒
指令延遲分析通常只有一個目的:讓程式碼跑得更快。《Assembly Hall of Shame》反過來做——其中一條規則寫得很白:「only a single instruction is eligible to be scored」,找的不是捷徑,是單一指令效能的絕對下限。維護者是 Christopher Domas(@xoreaxeaxeax),排行榜目前有 27 個條目,從 1 個週期的 nop 排到 198,002,498,236 個週期的 fxrstor64——同一套 x86 指令集裡,延遲橫跨十一個數量級。
只算一條指令,其餘鋪陳全部免費
五條規則定義了什麼叫「合法」的單一指令測量。第一條最寬鬆:「Instructions may use whatever setup is necessary, but only a single instruction is eligible to be scored」——計時之前,暫存器灌好、記憶體排好、快取熱好都不算數,只要最後計時範圍只包住一條指令。第二條收得比較緊:「Trapped/emulated/virtualized instructions may only time the trap, not the handler」,一條指令若觸發例外或被虛擬化層接手,能算進成績的只有「進入 trap」那一段,後面的處理常式跑多久不算。第三條直接排除一整類指令:「Instructions must not be interruptible. rep movs, pause, etc. are disqualified」,凡是執行中途可以被打斷的指令一律出局。第四條管計量單位:「Times are normalized based on the CPU base clock frequency」,逼著每一筆成績都能換算回同一把尺。第五條另外管硬體狀態:「All platforms must be in their factory stock configurations - no hardware modifications」,擋掉超頻降頻這類取巧路線。
第二條與第三條規則的用意,合理的推測是要把「指令本身的微架構成本」跟「作業系統或例外處理的成本」切開:如果只計進入 trap 而不計處理常式,任何指令只要能誘發一次分頁錯誤、把處理常式導向從磁碟讀回一整頁記憶體,就能輕鬆宣稱自己執行了數毫秒——那樣量到的其實是磁碟延遲,不是指令本身。可中斷指令的問題類似:rep movs、pause 這類指令執行中途可以被中斷再恢復,作業系統排程器隨時可能插進來處理別的工作,中斷期間的等待時間會被算進總延遲,量到的會是排程器的行為,而不是指令本身的微架構成本。
硬體本身也是排行榜的一部分訊息。榜單上出現三種完全不同的機器:AMD Ryzen 7 5800H 撐起大多數高名次條目(vmovdqu 系列、mov 系列,以及 fxrstor64 的兩個版本都在它身上量出來),Intel i7-8559U 包辦中段大多數與微碼相關的條目(idiv、enter、fsin、mfence、cpuid、rdrand、fdiv 等),VIA Eden 800MHz 這顆罕見的舊平台則貢獻了 rdmsr 一項。三顆時脈完全不同的處理器能放進同一張表比較,靠的正是「依 base clock 正規化」這條規則——把週期數換算回以基準時脈為單位的秒數,才能讓 800MHz 的 VIA Eden 跟現代的 Ryzen 站上同一把尺。
五條規則定出了邊界,卻沒有限制「能把周圍環境操縱到多誇張」,這正是排行榜跨越十一個數量級的原因。下面這把尺從最快的 nop(1 個週期、0 奈秒,Intel i7-8559U 量出來的)一路拉到目前的榜首 fxrstor64(198,002,498,236 個週期、正規化後 62 秒,AMD Ryzen 7 5800H)。拖著滑桿走一遍會發現,中段的條目幾乎全部卡在奈秒與微秒之間,真正拉開差距的是最後兩名——兩個版本的 fxrstor64,數字直接跳進秒的量級。
拖曳滑桿走過 27 個名次 · 對數刻度橫跨十一個數量級
多數指令延遲表格量的是「一般情況下要多久」;這份排行榜反過來問「最壞情況下最多要多久」,只要沒違反五條規則,壞到什麼程度都算數。問題來了:同樣站在這套規則底下,一條指令要多繞幾圈才會落到榜單前段?答案分成兩條路——一條往核心裡面繞,一條往核心外面繞。
假設一——把微碼路徑走到最深
先看第一條路:讓指令在核心內部的微碼裡繞更長的路。idiv 排在前段(77 個週期、28 奈秒),策略寫得直白:「Use 128-bit dividend (rdx:rax=2:0) with small divisor to push the quotient above the ceiling imposed by sign-extension, driving the longest path through the divider microcode」——把被除數塞到 128 位元的上限、除數壓得很小,逼出商數超過符號延伸能處理的範圍,除法器微碼因此得走最長的那條路徑。同一顆除法電路,換一組刻意刁鑽的運算元,就從一般除法滑進最慢除法。
enter 的策略更明確地把路徑長度跟一個可調參數綁在一起:「Use maximum nesting depth (31) to force 30 display-pointer loads and pushes through the microcode display-walk path」。x86 的 enter 指令依巢狀層級建立一串 display(詞法作用域指標陣列),每往下一層就要多讀寫一次上層的 frame pointer;把巢狀深度拉到 x86 允許的上限 31,微碼就得老老實實搬 30 次。
拖曳把手調整巢狀深度 · 最深 31 層
fsin 走的是另一種微碼分支——不是巢狀深度,是運算元本身的特殊性:「Use exponent 0x7ff to reach 'special value' processing in microcode; positive/negative, NaN/inf doesn't seem to make a difference, go with QNaN」——指數欄位撐到 0x7ff,就會觸發微碼裡的「特殊值」處理路徑;策略作者試過正負號、NaN、無限大幾種組合,發現差別不大,索性固定用 QNaN。
切換運算元,比較快速路徑與微碼特殊值路徑 · 2 種狀態
三個條目、三種微碼繞路方式,最後停在 28 奈秒到 94 奈秒之間(idiv 28 奈秒、enter 41 奈秒、fsin 94 奈秒),比起後面要看到的秒級數字,連零頭都不到。核心內部能繞的路畢竟有限,要繼續往上爬,得往核心外面找資源。
假設二——把周邊資源榨乾
第二條路不在核心裡面找,而是去搶核心外面、大家共用的資源。四個條目剛好對上四種不同的共用資源:write-combining line-fill buffer 是核心送出資料前排隊的緩衝區,cpuid 的 leaf 空間是微碼內建的一張查找表,rdrand 背後的熵源是一顆實體電路,MSR 則可能連到別的晶片——四種資源,四種榨乾的手法。
mfence 的策略是先把緩衝區灌滿:「Saturate all write-combining line-fill buffers with movnti stores to distinct cache lines, forcing mfence to drain the full LFB write path to the uncore before retiring」。用 movnti 把不同快取線的寫入通通塞進 write-combining line-fill buffer,等全部佔滿,mfence 作為一個記憶體屏障指令,必須等這些緩衝區清空、寫入送達 uncore 才能退休——它本身沒有變慢,是它必須等的隊伍變長了。
cpuid 的策略讀起來像暴力搜尋:「Use rakefield to find the highest latency CPUID leaves」——rakefield 聽起來是作者自己寫的掃描工具,把 cpuid 支援的每個 leaf 都跑過一輪,量出延遲分布,再挑出最慢的那幾個提交上榜單,這跟 wrmsr 提到的 project:nightshyft 是同一種套路:先掃描,再挑異常值。合理的推測是,不同 leaf 延遲不同,跟每個 leaf 背後要組裝的資訊複雜度有關——查詢廠牌字串這類簡單 leaf 微碼路徑可能很短,涉及拓樸、TLB 結構這類需要彙整硬體多個角落資料的 leaf,組裝工作可能明顯更多;README 本身沒有解釋差異從何而來,只交代了用什麼工具去找。
rdrand 打的是另一種資源——熵池:「Execute in a tight loop to deplete the hardware entropy pool faster than it can be refilled, forcing subsequent calls to stall while the entropy source recovers」。硬體亂數產生器背後接的是一顆實體熵源,補充速度有物理上限;在緊湊迴圈裡連續呼叫 rdrand,消耗速度超過補充速度,熵池見底,後續呼叫就得停下來等它回血。rdrand 底層接的是硬體亂數產生器,通常由一個物理熵源加上一段確定性隨機位元產生器組成,熵源本身補充亂數的速度受限於物理電路本身,不會因為程式呼叫得勤就跟著加快。平常呼叫 rdrand 延遲很低,是因為熵池通常維持在接近滿的狀態;策略描述裡刻意做的事,就是不斷抽,不給熵源喘息的機會。
wrmsr 這一條的策略描述最長,也最貼近搶外部資源的核心:「Use project:nightshyft to identify high latency MSRs. MCG_CTL on Zen look like a winner: may be a microcode quiesce and synchronize on MCA error banks across hardware units, some potentially off-die, requiring fabric-level communication rather than a simple local register write」。原始策略的用詞本身就是三重保留:「may be」「look like」「potentially」——連提出這個解釋的作者自己都沒有把話說死。照這個說法,在 AMD Zen 上寫 MCG_CTL 這個 MSR,可能不只是改一個本地暫存器,背後也許觸發微碼靜默、也許跨硬體單元同步 MCA 錯誤庫;如果屬實,某些參與同步的單元可能不在同一顆晶粒上,才需要 fabric 層級的通訊,而不是一次單純的本地寫入。這條全排行榜裡少見地連作者自己都在猜的條目。
點擊四張卡片,比較各自榨乾哪一種共用資源 · 4 個條目
mfence · write-combining line-fill buffer
把不同快取線的 movnti 寫入灌滿所有 write-combining line-fill buffer,逼 mfence 得等這些緩衝區清空、資料送達 uncore 才能退休。
mfence 卡在快取子系統,cpuid 卡在微碼查表的深度,rdrand 卡在物理熵源,wrmsr 卡在跨晶片同步——四個條目分別榨乾四種不同的共用資源,結果全部停在微秒級(mfence 0.12 微秒到 wrmsr 10.74 微秒)。兩種假設都成立,卻都還差六到七個數量級才追得上榜首。
往上爬過 wrmsr 之後,中段其實一樣有策略描述,不是「指令本身性質」單方面決定的。out(15.58 微秒):「Target an I/O port that straddles a NIC device register boundary, triggering the device to quiesce its TX DMA engine on each write」——挑一個橫跨網卡裝置暫存器邊界的 I/O 埠,每次寫入都逼裝置暫停 TX DMA 引擎。換句話說,等待的不是匯流排,是網卡自己:每次寫入都讓裝置暫停它自己的傳送 DMA,CPU 端反而沒有變慢。rdmsr(0.20 毫秒,量測平台是 VIA Eden 800MHz):「Use project:nightshyft to identify high latency model-specific-registers: VIA uses an undocumented register at 0x133 that gives wildly high response time」——同樣用 project:nightshyft 掃描,在 VIA 平台上找到一個沒有公開文件記載、位址 0x133 的暫存器。這是全榜單唯一一個在 VIA Eden 上量出來、也是唯一沒有在兩顆現代處理器(AMD Ryzen、Intel i7-8559U)上重現的條目。wbinvd(0.51 毫秒):「Fully load L1/L2/L3 caches with dirty lines to force DRAM writeback of entire hierarchy」——把三層快取全部塞滿骯髒(dirty)快取線,逼整條階層寫回 DRAM。跟 clflush 那種只清一條快取線不同,wbinvd 逼的是整個三層快取的容量,等待時間因此綁定在 DRAM 頻寬能不能吃下這整批寫回,而不是單一條快取線的延遲。in(3.92 毫秒):「Target I/O port mapped to an ACPI PM block where an unaligned 4-byte read decodes into multiple non-posted loads from wherever this port goes」——瞄準 ACPI 電源管理區塊的 I/O 埠,一次不對齊的 4 位元組讀取被解碼成好幾筆非入列讀取。問題不在讀取指令本身,而是位址沒有對齊:同一次讀取被拆成好幾筆各自要等待完成的非入列存取,而不是一次就結束。
mov、mov rax、vmovdqu 三個系列走的是同一套路:先在 PCIe fabric 裡搜尋一個延遲異常高、身分不明的 GPU 暫存器,再用不同寬度的讀取去踩它。mov(0.14 秒)的策略只有一句:「Use mmiotic to identify high-latency deadspace in PCIe fabric, hit unkown GPU register」。mov rax(0.28 秒,8 位元組讀取、兩次 dword 存取)、vmovdqu xmm(0.56 秒,16 位元組、四次 dword 存取)、vmovdqu ymm(1.11 秒,32 位元組、八次 dword 存取)、vmovdqu ymm unaligned(1.39 秒,32 位元組不對齊、九次 dword 存取)用的幾乎是同一句策略模板:「Search MMIO space for slowest registers in PCIe fabric, hit unknown GPU register, use N-byte MMIO read to get M dword register accesses, which isn't technically allowed but works anyway」——讀取寬度越寬,一次踩到的暫存器存取次數越多,成績也跟著往上爬;vmovdqu 系列的策略原文甚至自己承認這種存取「isn't technically allowed」,卻照樣量得出成績。aligned 版本的 32 位元組讀取踩出八次 dword 存取,unaligned 版本因為起始位址沒有對齊,同一段資料被切過一道額外的邊界,變成九次——多出來的那一次,就是不對齊付出的代價。再往下靠近奈秒等級,策略描述都短得像一句話:fdiv(0.33 微秒):「Use subnormal divisor, hardware hands control to microcode assist, assist normalizes operand, performs the division, then restores architectural state」——次正規化(subnormal)除數逼硬體把控制權交給微碼協助例程。split lock(0.32 微秒):「Align lock-prefixed operand to straddle cache-line boundary, forcing CPU to assert the external bus lock rather than using the fast MESI cache-coherence path」——把帶 lock 前綴的運算元對齊到剛好跨越兩條快取線,逼處理器放棄快速的 MESI 協定,改用外部匯流排鎖定。fadd(denormal,0.25 微秒):「Hit x87 FP microcode assist path by using denormal source operand」——跟 fdiv 是同一種機制的不同觸發點。mov cr3(0.11 微秒)的策略欄位寫得很隨性:「Nothing for now, just check how long it takes to invalidate the TLB」——什麼手腳都沒動,純粹量 TLB 失效要多久。clflush(60 奈秒):「Just ensure the cache line is dirty」。fldl(49 奈秒):「Try a small denormal to trigger an FP microcode assist」。mov cr3、clflush、fldl 這三條的策略欄位是全榜單最短的,但仍然是刻意的操縱手法,不是「什麼都沒做」。rdtsc(18 奈秒)連操縱手法都沒有:「Just a reference instruction to get our bearings」——作者自己說明這條只是拿來定位其他成績的參考點。這一長串條目共同的模式很清楚:真正拉開名次的從來不是指令本身天生就慢,而是找到一個會讓硬體走進慢路徑的觸發條件——微碼協助路徑、快取一致性協定的例外分支、未知的 MMIO 目標、裝置狀態切換、罕見平台上未公開的暫存器;越往榜首爬,操縱的對象從指令內部走到指令要碰到的裝置與周邊子系統,跟接下來要看的 fxrstor64 冠軍走的是同一個方向。
冠軍策略——把兩件事疊在一起
真正衝上榜首的 fxrstor64,基準版本(排行榜第 2 名)的準備工作其實不算少:「Use mmiotic to identify high-latency deadspace in PCIe fabric, isolate region near 0's and offset state to avoid MXCSR corruption (possibly VGA buffer?), use fxrstor64 to load 512-byte FPU/MMX/XMM state from MMIO, forcing CPU to process 512 bytes of I/O transactions through slowest available memory aperture」——先用 mmiotic 這個工具在 PCIe fabric 裡找出高延遲的死區(deadspace),把目標區域鎖定在位址接近 0 的地方,還特地把狀態偏移過以避免弄壞 MXCSR(作者自己猜測那塊區域可能是 VGA 緩衝區,語氣帶著問號),最後才讓 fxrstor64 從這段 MMIO 讀回 512 位元組的狀態,逼 CPU 把這筆 I/O 交易全部塞進最慢的那段記憶體孔徑。這一長串準備工作本身也符合第一條規則的「setup 免費」——真正計時的只有那一行 fxrstor64,前面找死區、避開 MXCSR corruption 的過程都不算分。最終量出來的成績是 74,584,168,512 個週期,正規化後 23.35 秒,這已經是核心內部微碼路徑或單一共用資源爭奪完全比不上的量級。
真正的冠軍策略,是在這個基礎上再疊一層爭奪:「Extend fxrstor64 (baseline) by starving the fabric while the load is in flight, a fleet of hammer cores pounds a different high-latency MMIO register with tight 4-byte reads, saturating the PCIe root complex and endpoint with non-posted transactions, so CPU 0's 512-byte fxrstor64 must queue behind all that contending traffic」。CPU 0 讀取那 512 位元組的同時,一批 hammer core 對另一個高延遲 MMIO 暫存器發出 4 位元組的小型讀取,把整個 PCIe root complex 與端點灌滿非入列(non-posted)交易,fxrstor64 這種同樣得走 non-posted 路徑的大型讀取,只能排在後面。最終量出來的成績是 198,002,498,236 個週期、62 秒,比基準版本慢了 2.65 倍。這 2.65 倍不是靠更複雜的操作換來的——冠軍版本跟基準版本執行的是同一行組合語言,差別純粹在旁邊有沒有其他核心搶匯流排;這也呼應排行榜規則裡「setup 免費」的精神,只要計時窗口只包住那一行 fxrstor64,旁邊的核心愛做什麼都不犯規。
切換 hammer cores 開關、播放看排隊如何變長 · 兩種真實量測情境
進度條依真實量測的兩個錨點縮放——關閉 hammer cores 對應基準版本 23.35 秒,打開對應冠軍版本 62…
其他核心不斷讀取同一個 PCIe 裝置時,fxrstor64 的 512 位元組讀取得排隊——同一顆核心、同一行指令,測得時間從 23.35 秒堆到 62 秒。
把兩個假設擺在一起看,fxrstor64 的優勢是同時吃到兩種效應:它本身走的是一條真的很慢的架構路徑——跨 PCIe 的 MMIO 讀取,而不是核心內部的暫存器存取,又疊上刻意製造的最大程度外部資源爭奪,整個 root complex 被灌到飽和。idiv、enter、fsin 只能繞核心內部的路;mfence、cpuid、rdrand、wrmsr 只能搶一種共用資源;fxrstor64 兩邊都占,才把成績從微秒級一口氣推進秒級。
這也點出評估任何單一指令基準測試時該問的問題:數字本身從來不是指令集的固有屬性,而是「指令本身+當下微架構狀態+周邊系統負載」三者共同決定的結果。同一行 fxrstor64,光是基準版本與冠軍版本之間,測得的差距就有 2.65 倍,而兩者唯一的差別只是旁邊有沒有其他核心搶匯流排——合理的推測是,任何宣稱「量到某條指令的延遲」的基準測試,如果沒有交代測量當下的系統負載狀態,那個數字的可比性就要打上問號。
Take-away:「一條指令要多久」不是指令集手冊上的常數,而是微架構路徑深度與外部資源爭用共同決定的變數——下次看到單一指令 benchmark 的數字,先確認兩件事:時脈有沒有正規化,量測當下旁邊有沒有別的東西在搶同一段匯流排。