後端工坊 2026 年 5 月 9 日

2026-05-09 — Uber Go 堆疊最佳化省 16% CPU、jank 自訂 IR 48 倍加速、C++26 編譯期反射 JSON

primary=https://www.uber.com/tw/en/blog/zero-growth-stack/ primary=https://jank-lang.org/blog/2026-05-08-optimization/ primary=https://isocpp.org/blog/2026/05/json-and-cpp26-compile-time-reflection-a-talk-daniel-lemire

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。

原始來源:Uber Engineering Blog


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 一貫的高吞吐量特性。

原始來源:isocpp.orgsimdjson GitHub


End of article
0
Would love your thoughts, please comment.x
()
x