編按:本文綜合整理自 Raymond Chen 微軟 DevBlogs 原文、Hacker News 討論串,並加入 Siami 編輯部觀點與分析。
256KB 程式碼只為清空 64KB 記憶體
微軟 DevBlogs 的 Raymond Chen(也就是The Old New Thing那個經典部落格的作者)這次講的是一段他同事口述、年代久遠的「x86 模擬器戰爭故事」。
故事的時空背景是 Windows 還在跑非 x86 原生處理器的年代——Raymond 沒有明說是哪顆 CPU,但微軟當年的硬體名單大家都猜得到:Alpha、MIPS、PowerPC、Itanium 等等,那是一段「Windows NT 跑在各種 RISC 架構上」的歷史。
那個年代 Windows 內建一個 x86-32 的處理器模擬器,讓原生跑別顆 CPU 的機器也能跑 x86 軟體。這個模擬器不是用直譯器(interpreter)逐行翻譯,而是用二進位轉譯(binary translation)——直接把 x86 指令即時編譯(JIT)成原生指令。
然後團隊撞見一段非常生氣的程式碼:
某支程式需要在 stack 上配置 64KB 的緩衝區並做初始化。正常編譯器會這樣做:
- 先做 stack probe 確認 64KB 空間存在
- 把 stack pointer 減 65536
- 用一個緊湊的迴圈逐 byte 初始化
但寫這段程式的那個編譯器(Raymond 沒明說是哪一家)覺得「用迴圈太遜了」——它決定把迴圈整個展開(loop unrolling),生成 65,536 條獨立的「寫一個 byte 到記憶體」指令,每條指令 4 bytes。
結果:256KB 的程式碼,只為了初始化 64KB 的資料。
這在當年是很真實的痛點:loop unrolling 是 90 年代編譯器優化教科書的標準技巧,但把 buffer-fill 迴圈展開成 65,536 個 MOV 指令,是教科書不會教、編譯器作者喝了幾杯才寫得出的奇葩產物。
模擬器團隊的反擊:把「爛程式碼」即時改掉
Raymond 的同事當然不是坐視不管。模擬器團隊被這段 256KB 的「展開迴圈」激怒到一個臨界點——他們決定在 binary translator 裡面加特殊規則:
「如果偵測到這段展開的爛迴圈,就直接把它換成等價的緊湊迴圈。」
也就是說,模擬器不再只是「忠實執行 x86」,而是變成「x86 編譯器的糾錯器」——它會在 runtime 把上游編譯器的離譜決定逆轉成有效率的版本。
這不是個案。Raymond 提到「This has happened many times」,意味著那支模擬器團隊長期在做**「接手爛程式碼、把它變快」**的服務。
為什麼這件事重要
這則故事表面上是懷舊趣談,實際上折射出三個至今仍然真實的系統軟體難題:
- 「優化」是雙面刃:loop unrolling、inlining、template metaprogramming 這些編譯器優化技巧,在某些 input pattern 下會變成「程式碼膨脹」甚至「效能倒退」。現代 LLVM 跟 GCC 都有開關可以關掉 unrolling(
-fno-unroll-loops),就是為了這類情境。 - 平台團隊被迫變成「產品 QA」:當微軟的模擬器團隊要開始「修」上游編譯器的 bug,這代表商業平台公司(不是 compiler 團隊)正在承擔 compiler 生態系的後果。這跟 Apple Silicon 早期 Rosetta 2 主動幫 Intel binary「修補」、Chromebook ARC++ 主動優化 Android app 的邏輯完全相同。
- 「年代相容性」是看不見的成本:Raymond 自己也說「This has happened many times」。模擬器團隊沒有刪掉那些「壞特例」就直接出貨,意思是今天我們在 Apple Silicon 上跑 Intel binary、或是 Windows 11 跑 1995 年的 DOS 程式,背後都有一堆「當年不得不做的髮夾彎」在撐。
數據解讀
| 指標 | 數字 | 對比 |
|---|---|---|
| 原始程式碼大小 | 256 KB | 為了清空 64 KB |
| 展開後指令數 | 65,536 條 | 每條 4 bytes |
| 等效迴圈大小 | 約 16 bytes | xor + rep stosb |
| 程式碼膨脹比 | 16,000 倍 | 教科書級的「優化失敗」案例 |
質疑:Raymond 自己也說「don’t know which processor this particular story applied to」——這個故事可能經過多年的口耳相傳,數字略有誇大或失真。但**核心現象(編譯器把迴圈展開成災難)**在現代編譯器生態裡仍然存在,2024 年 LLVM mailing list 還有針對「過度 unrolling 導致 i-cache miss」的討論。這則故事作為「系統思維」的教材價值,遠高於它作為單一歷史事件的可考據性。
延伸案例:HN 社群補完的「同類災難」
這則故事在 HN 拿下 482 分、159 個留言,留言區串起了一整個世代工程師的共同創傷:
- 90 年代 Mac 顯示卡架構師:PageMaker 主動 invalidate 整個視窗卻不通知 redraw,迫使硬體團隊登門「拜訪」軟體團隊請求改寫。
- 68K Mac emulator 開發者:模擬器最難的不是讓舊程式跑起來,是讓它正確地慢下來——年代久遠的 polling loop 跟 CPU-burning 迴圈才是真正的鬼故事。
- Excel 內部:為了下相容 1990 年代的怪招,寧可保留最糟的「特例」也不動——這是現代 Office 肥大、難以重寫的根本原因之一。
- GTA Online:十幾分鐘的開機讀取,官方擺爛多年,最後是熱心玩家逆向工程出社群版修補工具。
這四個案例橫跨 1990 到 2020 年代,核心問題都一樣:商業軟體長期演進後累積的「技術債」沒有清理路徑,使用者、開發者、甚至平台團隊都被迫接手。Raymond 講的是編譯器產出垃圾碼,HN 留言區講的是整個產業的「編譯器產出垃圾碼」心態。
還沒解決的問題
這個故事沒有真的「被解決」——它只是被轉移:
- 編譯器升級 ≠ 程式碼重生:即使現代 LLVM 不會再展開 65,536 條 MOV 指令,那些 1990 年代編譯出來的 binary 仍然躺在使用者的硬碟裡,被模擬器一個一個救回來。
- 平台團隊的「隱形工時」:Raymond 沒有明說,但這暗示微軟模擬器團隊有一套完整的「客戶端程式碼品質資料庫」,專門記錄哪些常見軟體會觸發哪些特殊路徑。這種隱性成本很難量化,但它是商業平台公司的護城河之一。
- AI 程式碼生成會讓這更嚴重:當 LLM 開始大量生成程式碼,模擬器團隊要處理的「奇葩輸出」只會越來越多。Raymond 沒明說的故事裡,犯人好歹是「編譯器」——LLM 連編譯器都沒有,直接吐出「能跑就好」的字串。
延伸閱讀:Raymond Chen《The Old New Thing》部落格 累積了 20 年的 Windows 工程文化側寫,是少數能讓你看到「商業系統軟體決策背後真實原因」的一手來源。HN 討論串 裡的留言則補完了整個 90 到 20 年代的同類案例。
網友熱門留言 (5)