Uber 在 Go 中消除堆疊擴張:Profile-Guided Stack 最佳化節省 16% CPU
Uber Engineering · 2026-05-07
Uber 工程團隊於 2026 年 5 月公開其在 Go 執行期的堆疊分配最佳化成果:透過靜態分析 fleet-wide CPU profile 並針對每個函式預先設定最適堆疊大小,將旗下一個主要服務的 CPU 使用率從 181% 降至 149%,整體節省約 16%,在其 200 萬核心的 Go 服務規模下效益相當顯著。
原本的問題
Go 的 goroutine 預設以 2KB 初始堆疊啟動,當函式呼叫深度超過預留空間時,執行期會觸發 copystack 操作:分配一塊更大的記憶體(通常是原來的兩倍),複製整個堆疊,並更新所有指標。在 Uber 規模下,65% 的服務運行 Go,copystack 在高流量路徑上累積成可觀的 CPU 開銷。
問題的核心在於 Go 的自適應堆疊策略屬於全域性參數,無法針對個別高熱路徑函式調整。Uber 透過 private linking 暴露 Go 執行期的內部 startingStackSize 全域變數,在應用程式啟動時靜態設定更大的初始值。
採用的方法
Uber 建立了一套自動化 Profile-Guided 分析流程:fleet-wide CPU profile 收集 → 過濾含有 copystack 操作的追蹤 → 從 binary 提取 debug symbol → 分析組語指令計算每個函式的實際堆疊使用量 → 以 P99 測量值推薦最適大小。以其中一個 middleware 為例,分析器識別出其堆疊消耗為 2.6KB,最佳化後壓縮至 600 bytes。
最終設定將全域 startingStackSize 從預設 2KB 提升至 32KB,消除了大多數熱路徑上的重複擴張週期。記憶體代價為每個執行個體增加約 50MB 堆疊記憶體,在 16GB 容器限制下佔比不到 2%,屬於可接受的取捨。
實際效果
- CPU overhead from stack growth:從 ~10% 降至 <1%
- 整體 CPU 利用率:181% → 149%(約 16% 改善)
- 記憶體增加:~50MB/instance(在 16GB 容器中佔 <2%)
- 無需修改應用程式程式碼,純執行期層配置
這套方法依賴 Go 執行期的非公開內部變數,升級 Go 版本時需重新驗證 symbol 名稱,目前仍屬 Uber 的私有實作。Go 官方目前不提供此類 per-function 堆疊提示的公開 API。
jank 引入自訂 IR:Clojure 方言原生編譯器以 48 倍效能超越 JVM
jank Blog · 2026-05-08
jank 是一個以 C++ 實作的 Clojure 方言編譯器,目標是在保留 Clojure 動態語意的前提下生成原生機器碼。2026 年 5 月,專案發布了自訂中間表示法(IR)的實作,以 fibonacci 基準測試為例,執行時間從 5,522ms 壓縮至 114ms,超越 Clojure JVM 的 200ms。
背景
IR(Intermediate Representation)是介於高階語言語意與目標 CPU 指令集之間的抽象指令集,為編譯器最佳化的核心舞台。jank 先前直接從 AST 生成 LLVM IR,跳過語言層最佳化的機會。自訂 IR 的設計層次高於 LLVM,能直接表達 Clojure 語意概念——變數解參考、動態分發、多型操作——而非低階記憶體操作。
核心改動
jank 的 IR 針對三類最佳化設計了對應的 pass:
- 指標標籤(Pointer Tagging):小整數直接編碼在指標低位元,避免堆積分配與裝箱(boxing)開銷
- 函式內聯(Function Inlining):在 IR 層識別純函式並展開呼叫,消除動態分發路徑
- 去除不必要裝箱:追蹤值的型別確定性,對已知為基本型別的值跳過 Clojure object 封裝
這些最佳化在 LLVM IR 層實作的難度遠高於在 Clojure 語意 IR 層,因為 LLVM 缺乏理解 Clojure 物件模型的上下文。自訂 IR 讓 jank 能在正確的抽象層次做正確的事,再將最佳化後的 IR 降至 LLVM 進行機器碼生成。
實際效果
| 版本 | fib(35) 時間 |
|---|---|
| jank(最佳化前) | 5,522ms |
| Clojure JVM | ~200ms |
| jank(自訂 IR 後) | 114ms |
jank 目前仍在早期開發階段,IR pass 框架是後續更多最佳化的基礎,包含逃逸分析、尾呼叫最佳化等規劃中的功能。
原始來源:jank Blog
C++26 編譯期反射實戰:simdjson 擴充以零執行期開銷序列化結構體
isocpp.org · 2026-05-06
Daniel Lemire 在 CppCon 2025 展示了如何利用 C++26 的編譯期反射(compile-time reflection)特性擴充 simdjson,讓結構體的 JSON 序列化與反序列化在零額外執行期開銷的前提下自動生成,不需要宏展開、手寫序列化函式或外部程式碼生成工具。
C++26 反射的核心機制
C++26 引入 std::meta 命名空間與 ^(reflect)運算子,讓程式設計師在 consteval 上下文中取得型別的完整元資料:成員名稱、型別、偏移量、基類列表都成為可查詢的編譯期值。例如 [:std::meta::members_of(^MyStruct):] 展開為所有成員的 reflection range,可在 consteval 函式中迭代並生成對應的 JSON key-value 對應邏輯。
關鍵在於這些查詢全部在編譯期完成,生成的序列化程式碼與手寫版本在組語層等效,不存在反射的執行期查詢開銷——這是 C++ 反射區別於 Java/Python 執行期反射的根本差異。
規格細節
simdjson 擴充的使用介面極為簡潔:只要結構體的成員型別皆為 simdjson 支援的基本型別或巢狀結構體,即可直接呼叫:
auto doc = parser.iterate(json);
MyStruct obj;
doc.get(obj); // 自動反序列化,無宏序列化路徑同理:to_json(obj) 在編譯期展開所有成員的名稱與型別,生成對應的 JSON 輸出邏輯。與 Boost.PFR 等現有方案的差異在於 C++26 反射能取得成員的原始名稱字串,無需依賴特定 ABI 技巧或限制型別為 aggregate。
影響範圍
C++26 規格預計於 2026 年末最終確定,GCC 15 與 Clang 20 均已有實驗性反射支援。simdjson 的反射擴充需要 C++26 相容編譯器,目前以實驗性 flag 啟用。對於已大量使用 simdjson 的後端服務,這條路徑有機會取代現有的手寫 from_json/to_json 函式,同時維持 simdjson 一貫的高吞吐量特性。