[推薦] 為何 rust 編譯這麼慢

Source

https://sharnoff.io/blog/why-rust-compiler-slow

Summary

作者針對其 Rust 網站的 Docker 建置速度慢問題進行了一系列優化,以下是詳細的優化措施、針對的問題以及時間變化:

  • 初始問題:作者的網站主要由一個 Rust 二進位檔提供服務,每次變更都需要重新建置、複製到伺服器並重新啟動網站,這是一個耗時的過程。使用 Docker 進行建置時,從零開始建置約需要 4 分鐘 (包括 10 秒用於下載 crates)。

作者進行的優化步驟及效果如下:

  • 問題:慢速建置及緩存無效

    • 原始時間:約 175 秒 (使用 cargo build --timings 測量為 2m 54s)。
    • 優化 1:導入 cargo-chef 以優化 Docker 緩存
      • 解決問題:在 Docker 建置過程中,每次更改都會從頭開始重建所有內容。cargo-chef 旨在預先建置所有依賴項作為單獨的 Docker 建置緩存層,從而使程式碼庫的更改僅觸發程式碼庫的重新編譯,而非依賴項。
      • 結果:建置依賴項耗時 1m 07s,但最終二進位檔的建置仍耗時 2m 50s。整體建置時間降至約 175 秒 (約 2 分 54 秒),主要瓶頸轉移到最終二進位檔的編譯。
  • 問題:LTO (Link-Time Optimization) 和除錯符號造成編譯時間過長

    • 原始時間:約 175 秒
    • 優化 2:停用 LTO 和除錯符號
      • 解決問題:透過 rustc 的自我分析功能發現,LTO (尤其是 “fat” LTO) 和完整的除錯符號顯著增加了編譯時間。作者原本在 Cargo.toml 中設定了 lto = "thin"debug = "full"
      • 更改:將 lto 設定為 "off",並將 debug 設定為 "none"
      • 結果:編譯時間從 175 秒降至 51 秒 (降低 71%)。
  • 問題:LLVM_module_optimize 仍佔用大量時間

    • 原始時間:約 51 秒
    • 優化 3:調整最終二進位檔的優化等級 (opt-level)
      • 解決問題:即使停用 LTO 和除錯符號,編譯最終二進位檔仍需 50 秒,其中約 70% 的時間花費在 LLVM_module_optimize 上。
      • 更改:將最終二進位檔的 opt-level 設定為 1 (預設 release 設定為 3),但保留依賴項的 opt-level3。雖然將 opt-level 設定為 0 可將時間降至約 15 秒,但為了保留部分優化,作者選擇了 opt-level=1
      • 結果:編譯時間從 51 秒降至 48.8 秒 (降低 4%)。
  • 問題:LLVM 的 InlinerPass 耗時過長

    • 原始時間:約 48.8 秒
    • 優化 4:使用 LLVM 參數降低內聯程度
      • 解決問題:分析 LLVM 追蹤事件發現,優化 ( OptFunction) 和內聯 ( InlinerPass) 佔用了大量時間。
      • 更改:透過 RUSTFLAGS 設定 LLVM 參數,將內聯閾值 (例如 --inline-threshold, --inlinedefault-threshold, --inlinehint-threshold) 從預設值降低至 10
      • 結果:編譯時間從 48.8 秒降至 40.7 秒 (降低 16%)。
  • 問題:大型異步函數的優化耗時過長

    • 原始時間:約 40.7 秒
    • 優化 5:拆分主程式碼中耗時的函數 (特別是異步函數)
      • 解決問題:作者發現大型異步函數在 LLVM 優化階段耗時過長,因為 Rust 編譯器將異步函數內部表示為巢狀閉包和複雜的狀態機。例如,PhotosState::new 函數的優化時間為 5.3 秒。
      • 更改:重構 PhotosState::new 等異步函數,將 Future 轉換為特徵對象 (Pin<Box<dyn Future>>),以隱藏實作細節並簡化狀態機。
      • 結果PhotosState::new 的優化時間從 5.3 秒降至 2.14 秒。此項優化連同下一項「依賴項相關更改」共同將總編譯時間從 40.7 秒降至 37.7 秒 (降低 7%)。
  • 問題:依賴項中的泛型導致重複編譯

    • 原始時間:約 37.7 秒
    • 優化 6:從依賴項中移除泛型或建置非泛型版本
      • 解決問題:由於泛型函數在其實例化的 crate 上下文中進行優化,導致即使是來自其他 crate 的泛型函數也會在主 crate 的上下文中重複編譯和優化。
      • 更改:作者對 pulldown-cmark 中的一個泛型函數進行了非泛型化,並為 lol_htmldeadpool_postgres 建立了單獨的本地 crate,公開其非泛型版本的 API。
      • 結果:這項優化使得總編譯時間降至 32.3 秒 (降低 14%)。
  • 問題:預設不共享泛型實例

    • 原始時間:約 32.3 秒
    • 優化 7:啟用 -Zshare-generics 旗標 (2025-06-27 更新)
      • 解決問題:Rust 編譯器預設不會為 release 建置啟用泛型實例的重複使用,這會對程式碼生成產生負面影響。
      • 更改:在 RUSTFLAGS 中添加 -Zshare-generics 旗標。
      • 結果:總編譯時間從 32.3 秒降至 29.1 秒 (降低 10%)。
  • 問題:Alpine Linux 的預設記憶體分配器對建置時間的影響

    • 原始時間:約 29.1 秒
    • 優化 8:從 Alpine 轉換到 Debian (2025-06-27 更新)
      • 解決問題:Alpine Linux 使用的預設 musl 分配器可能會對建置時間產生重大影響。
      • 更改:將 Docker 基礎映像從 Alpine 切換到 Debian,並移除 --target=x86_64-unknown-linux-musl
      • 結果:這項優化產生了顯著的影響,總編譯時間從 29.1 秒大幅降至 9.1 秒 (降低 69%)。

總結如下

  • 初始時間:約 175 秒
  • 停用 LTO 和除錯符號:降至 51 秒 (-71%)
  • 將最終 crate 的 opt-level 設為 1:降至 48.8 秒 (-4%)
  • 使用 LLVM 參數降低內聯:降至 40.7 秒 (-16%)
  • 本地程式碼更改:降至 37.7 秒 (-7%)
  • 依賴項相關更改:降至 32.3 秒 (-14%)
  • 啟用 -Zshare-generics:降至 29.1 秒 (-10%)
  • 從 Alpine 轉換到 Debian:降至 9.1 秒 (-69%)
cmd + /