[推薦] 為何 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 秒),主要瓶頸轉移到最終二進位檔的編譯。
- 解決問題:在 Docker 建置過程中,每次更改都會從頭開始重建所有內容。
- 原始時間:約 175 秒 (使用
-
問題: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-level
為3
。雖然將opt-level
設定為0
可將時間降至約 15 秒,但為了保留部分優化,作者選擇了opt-level=1
。 - 結果:編譯時間從 51 秒降至 48.8 秒 (降低 4%)。
- 解決問題:即使停用 LTO 和除錯符號,編譯最終二進位檔仍需 50 秒,其中約 70% 的時間花費在
-
問題:LLVM 的
InlinerPass
耗時過長- 原始時間:約 48.8 秒。
- 優化 4:使用 LLVM 參數降低內聯程度
- 解決問題:分析 LLVM 追蹤事件發現,優化 (
OptFunction
) 和內聯 (InlinerPass
) 佔用了大量時間。 - 更改:透過
RUSTFLAGS
設定 LLVM 參數,將內聯閾值 (例如--inline-threshold
,--inlinedefault-threshold
,--inlinehint-threshold
) 從預設值降低至10
。 - 結果:編譯時間從 48.8 秒降至 40.7 秒 (降低 16%)。
- 解決問題:分析 LLVM 追蹤事件發現,優化 (
-
問題:大型異步函數的優化耗時過長
- 原始時間:約 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%)。
- 解決問題:作者發現大型異步函數在 LLVM 優化階段耗時過長,因為 Rust 編譯器將異步函數內部表示為巢狀閉包和複雜的狀態機。例如,
-
問題:依賴項中的泛型導致重複編譯
- 原始時間:約 37.7 秒。
- 優化 6:從依賴項中移除泛型或建置非泛型版本
- 解決問題:由於泛型函數在其實例化的 crate 上下文中進行優化,導致即使是來自其他 crate 的泛型函數也會在主 crate 的上下文中重複編譯和優化。
- 更改:作者對
pulldown-cmark
中的一個泛型函數進行了非泛型化,並為lol_html
和deadpool_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%)。
- 解決問題:Alpine Linux 使用的預設
總結如下:
- 初始時間:約 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%)
發佈時間
2025-6-27