我喜歡 Rust 已經很久了,它的優點很明顯,同時缺點也很明顯。內存 Safe 無需 GC、零成本抽象、並發安全、幾乎現階段最好的工具鏈和配套外圍建設、超強的覆蓋面(上到 Web 雲伺服器,下到單片機),還有錯誤處理設計非常好。缺點嘛,學習陡峭?難以理解 Ownship,Borrow 之類的模型約束;寫起來很慢?

長久以來,我們都認為這是 Rust 的缺點,但是實際上前兩者都是可以透過鍛鍊思維和認知來解決的,而編譯速度慢卻一直拖慢著我的工作流,這一切在早幾年似乎很好,AI 席卷而來之前我可有的就是耐心,常常一個演算法一個功能寫到凌晨,可,現在呢?我很難說我沒有變,我開始變得一點都沒有耐心了,一點都不想思考 Rust 代碼怎麼寫,一點都等不了 Cargo 那個漫長的進度條,尤其是 ld 部分還沒有進度,每次就干卡著,這真的很難受。

這一切難道就沒有辦法了嗎?其實是有的。在大約一個月前,我嘗試了一些辦法讓我的 Rust 開發流程更快,包括工作流程優化之類的,但其實每次等待的時間都卡在 Cargo 上。如果不能解決這個就無法治本,那正好藉這個機會來稍微說一下關於 Rust 開發中 Cargo.toml 設定調優相關的內容和配套週邊設定吧。

後端理論

在 Rust 的設計中,LLVM 雖然有點偷懶,但選擇它是明智的,與 Go 自行維護產生機器碼的過程不同,讓 LLVM 這個龐大的生態系統來負責成熟的最佳化實際上效果很好。Rust 的整體編譯流程大致如下:原始碼先進行詞彙分析與語法分析,產生 AST,接著經過巨集展開與 HIR 降階平坦化。之後執行一般的型別檢查以及少量的借用檢查,最終產生 MIR;到此為止,全部由 Rust 編譯器的前端負責。MIR 之後的工作交給 LLVM,例如將 MIR 轉譯成 LLVM IR,接著 LLVM 執行一系列的最佳化 pass(從 O0 到 O3 不等),最後產生目標平台的機器碼,交給連結器拼湊成可執行的 BIN 檔案。

.rs sourceLexer + Parser→ ASTMacro expansionDesugaringHIR loweringName resolutionType checkingTrait solvingBorrow checkerLifetime validationMIR generationCFG,const eval,inliningLLVM IR codegenTranslate MIR → IRLLVM optimizationsO0 ~ O3 passesMachine codeTarget-specificLinkerSystem linkerExecutable binary

那麼到底問題出在哪裡呢?答案其實已經很簡單了。LLVM 是一個很重型的基礎設施,重型在生產環境下是正確的,用編譯時間換取執行時間是非常好的哲學,它銜接了上層 Rust,以及 C、C++、Swift 等一大堆語言。這意味著它的最佳化管線是為「產生最優機器碼」而設計的,而不是為「快速完成編譯」而設計的。那數十個最佳化 pass 在 release 模式下執行一遍非常耗時,即便是 debug 模式下的 O0,LLVM 依然要走完 IR 產生和機器碼產生的完整流程,而這個流程本身就並不輕量。再加上 Rust 的泛型單態化會在編譯期展開大量程式碼,交給 LLVM 的 IR 體量遠比你原始碼看起來的要大得多,LLVM 需要處理的工作量自然也隨之膨脹。

Cranelift

所以本質上,編譯慢的很大一部分時間不是花在 Rust 自己的前端而是花在了 LLVM 這個"重型後端"上。那麼問題來了,有沒有可能丟掉它嘛,答案是有的喵,有一個叫做 Cranelift 的後端就是一個為了這個需求做的。它最早是 Cretonne,2016 年啟動,由 Bytecode Alliance 開發,最初是為 Wasmtime 包的餃子,才設計的程式碼生成後端;後被 Rust 官方收編作為可選的 codegen 後端。

一個低階可重目標程式碼產生器。 , github.com, opens in new tab
這是一張 Bytecode Alliance 的 Cranelift 文件頁面的螢幕截圖,頁面頂部有導覽列,連結至「Documentation(文件)」「API Reference(API 參考)」「Contributing(貢獻)」「Chat(聊天)」以及 GitHub 圖示。下方有大型的「Cranelift」標題,正文描述它是用 Rust 撰寫的快速、安全、相對簡單且創新的編譯器後端,能將中間表示編譯成可執行的機器碼,並被 Wasmtime WebAssembly 虛擬機用於 JIT 與 AOT 編譯,亦作為 Rust 編譯器的實驗性後端。文中指出 Cranelift 目前支援 x86-64、aarch64(ARM64)、s390x(IBM Z)以及 riscv64 平台,可重新定位並歡迎更多指令集貢獻,並說明它持續維護,以在沙箱環境中執行不受信任程式碼,提供接近原生效能,遵循 Wasmtime 的發佈與安全政策。此頁面證明了 Cranelift 的專案範圍、支援的架構,以及在 Bytecode Alliance 生態系統中的角色。

那所以 Cranelift 到底應該快在哪裡呢?最好的階段就是開發時期。這個時候也許你根本不需要接近完美的、極限優化執行效率的機器碼,也許我們只需要一個快速的反饋而已,只要這個不是那麼好的機器碼和 LLVM 正經算出來的機器碼語意等價就行了。早期的 Cranelift 其實做不到,因為有很多 Edge Case,雖然現在還是有,但是已經改善很多了,現在出現 Cranelift 可以跑但是 LLVM 壞掉的機率差不多和你寫 rust 觸發 rustc ICE 差不多了。LLVM 為了生成極致優化的機器碼,會跑幾十個優化 pass,比如迴圈展開、向量化、常數傳播、消除死程式碼之類的…… 很多很多,一層一層磨,而 Cranelift 的設計哲學完全不同,它大幅削減了這些優化步驟,只做最基本的暫存器分配和指令選擇,用一趟線性掃描就完成程式碼生成,不反覆迭代。同時它的 IR 設計也更輕量,是專門為快速從上層 IR 翻譯到機器碼設計的,不像 LLVM IR 那樣承載著幾十年的通用化包袱。

MIRLLVM IRMulti-layer IR100+ opt passesO0 ~ O3,LTOMachine codeComplex reg allocLinkMIRCLIF IRSingle-layer IRLightweight optsFewer passesMachine codeSimple reg allocLink

那麼好處說完了,代價呢?首先顯而易見的是 Cranelift 生成的程式碼執行期效能比 LLVM 差,根據場景不同大概會慢 10%~30%。但想開了其實就會發現,這在開發階段根本不重要,我要那麼快幹嘛,你是跑分還是 CI release 啊,我就 tm 要跑 cargo build 快點,看看邏輯出沒出來,而且我賭,你寫的大部分程式不可能 80% 以上的時間占滿計算資源,肯定是要有業務來的,你開發的時候業務能有多大?cpu 怕不是全程都在摸魚呢,反倒是我要看效果、驗證邏輯。那編譯時間換執行期效能,這筆帳在開發迴圈裡是不是應該倒過來算嘛。

程式碼生成單元

另外說說 codegen-units,這其實就是一個控制編譯器把 crate 拆成多少個最小單元給後端並行處理的參數,預設情況下,debug 模式是 256 個,release 模式是 16 個。數字越大,並行度越高,編譯越快——因為可以同時利用多個 CPU 核心來跑後端程式碼生成。但代價是優化效果變差,因為每個單元是獨立優化的,LLVM(or Cranelift)看到的上下文變小了,跨單元的內聯和優化機會就少了。

MIRCGU 1CGU 2CGU 3CGU …NLLVM opts + codegenLLVM opts + codegenLLVM opts + codegenLLVM opts + codegen.o.o.o.oLink

但是在真實的 Release 下,一般來說都應該使用 codegen-units = 1 這樣一個極端的參數,只有這樣才能讓產物得到最大化的優化,畢竟你都用 Rust 了,編譯時間換執行時效能難道不是理所當然的嘛。

優化等級

另一個經常被忽略但是可以調整的設定就是 opt-level,預設情況下應該是 "3",而作為 Release,更常見的做法是開到 "z",這就是要在不修改程式碼的前提下,可以獲得的免費產物體積最佳化,為什麼不開呢;但是呢,開發模式下這裡應該設定為 "0",完全不最佳化才能獲得最快的速度。

另外還有一個小技巧就是 Cargo.toml 其實可以給依賴和自己的程式碼設定不同的優化等級:

toml
[profile.dev.package."*"]
opt-level = 3

這樣就可以給第三方相依套件用 O3 編譯,這樣只會增加第一次冷編譯的時間,後續就是增量編譯的。外部相依套件一般不會頻繁更新,這裡用 O3 自己業務程式碼用 O0 可以獲得一般情況下權衡速度和體積的良好 dev 模式體驗,但是 Release 模式下我還是建議 O3 或者 Z 拉滿沒啥好說的。

LTO(Link‑Time Optimization)是另一項非常佔用記憶體與編譯效能的功能,而且大多數專案在 Release 時都建議開啟。正常編譯時,每個 crate 會獨立優化,編譯後端看不到 crate 之間的呼叫關係,某些跨 crate 的內聯與死碼消除做不到,這種情況相當常見;LTO 便是打破這個界限,讓優化器在連結時取得所有 crate 的 IR,進行全域最佳化。但千萬不要在 profile 上無腦掛 lto = true,這會讓你的 Rust 編譯變得慢到懷疑人生,只要在使用 release profile 時掛上即可。

去除除錯符號

和其他東西一樣,Rust 的 debuginfo 和 symbols 就是存在於這裡的,release 模式開啟 strip 可減少非常可觀的體積(通常是50MB vs 5MB)這種極大的差距,去除 symbols 也更加安全,畢竟大部分時候前端 source code 都是從 map 裡面不小心 push 上 npm 洩漏的 :(

所以 dev profile 下沒什麼好說的,你總不能 strip 掉 debuginfo 吧,那樣你就沒法用 gdb / lldb 正常調試了,backtrace 裡也看不到有意義的函數名。但是注意,ArchLinux 打包的時候會預設幫你 strip,但是這裡就會和我們有一點小摩擦,那就是我們已經 strip 掉了,後面可能會報錯。這個如果寫 AUR 的時候可以注意一下顯式跳過這一步。

Full binary.text.rodata / .data.symtab.debug_* (DWARF).eh_frame (unwind)strip=debuginfo.text.rodata / .data.symtab.debug_*.eh_frame (unwind)strip=symbols.text.rodata / .data.symtab.debug_*.eh_frame (unwind)+ panic="abort".text.rodata / .data.symtab.debug_*.eh_frame

放棄 panic!

在正常的業務中,panic 應該是不能存在的,正常業務程式碼 runtime 遇到錯誤的時候,但凡是設計了容錯的 Err 都應該被安全地傳回,而不是暴力 panic 掉,就和 React ErrorBoundary 一樣,應該被正常當作錯誤處理,panic 只有在程式碼堅信進入了不可能的狀態,且無法挽回的時候才應該觸發。我個人哲學是放棄 panic,為什麼呢?因為實際上業務程式碼可以作為 3 層測試,第一層 happy path,第二層錯誤路徑(無限種可能中的某一種),第三層才是 fuzz or 碰撞測試窮舉出來的 Edge Case。工程學上來說,正確路徑可以做到 100% 測試,錯誤路徑可以一類 n 種變體中取 1 種;碰撞測試純粹是時間問題,大部分時候不值得,等你的使用者體量真的上來了,要做的時候你自己就會知道,通常我一律不做。

因此,對於優質的程式碼而言,應該覆蓋一類正確路徑的測試與至少一種錯誤路徑的測試,而只要覆蓋此類錯誤路徑中的一個,即可測試 Err 被返回的情況,從而編寫 fallback 邏輯,自然可以 thiserror 列舉或 anyhow 截取並平攤列印記錄。總之,你的程式碼會迫使你為這些情況寫出處理機制;如果已經做到這一層,panic 對你就沒用了,可以理解為「理論上不可能」,但這僅止於理論。實際世界中仍有 OS 錯誤、記憶體錯誤、宇宙射線單電子位元翻轉、各種奇怪的溢位…… 這些情況永遠不可能全部覆蓋。那為什麼不使用 panic 呢?因為這些情況大多不是你的程式碼問題,約有 90% 的機率不是由你造成的。panic 的本質在於發生時,Rust 會沿著呼叫堆疊逐層回溯,依序呼叫每一幀的析構函式(drop)以釋放資源。這個過程需要編譯器產生額外的 unwind 表格,協助你找出業務邏輯錯誤的地方。如果錯誤本身大多不是你的,那這資訊的價值何在?它也會增加編譯產物大小,並給連結器帶來額外工作量,關閉才是正解;當你的程式碼測試到位且足夠自信時,建議設定 panic = abort

實際測試

那麼多說無益,實際來看看這些組合下的提升吧,這裡拿一個我前幾個月做的小玩具專案作為參考.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Language              Files        Lines         Code     Comments       Blanks
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 CSS                       5          152          113           16           23
 Go                       77        14556        12409          584         1563
 JavaScript               23         2506         1999          232          275
 JSON                     93         1780         1780            0            0
 Just                      1          408          275           71           62
 Makefile                  1           15            8            3            4
 Shell                    16         1440         1075          166          199
 SVG                       1            9            9            0            0
 TOML                     20          502          419            3           80
 TSX                      75         3257         2718          132          407
 TypeScript              390        36643        30350         1627         4666
─────────────────────────────────────────────────────────────────────────────────
 HTML                      2           41           32            7            2
 |- CSS                    1           37           37            0            0
 (Total)                               78           69            7            2
─────────────────────────────────────────────────────────────────────────────────
 Markdown                 95         5344            0         3578         1766
 |- BASH                   3            8            8            0            0
 |- Go                     2           21           21            0            0
 |- HTML                   1           25           15           10            0
 |- JSON                   6          317          317            0            0
 |- Rust                   1           10            9            0            1
 |- TSX                    1           11            9            0            2
 (Total)                             5736          379         3588         1769
─────────────────────────────────────────────────────────────────────────────────
 Rust                    181        31654        26951         1032         3671
 |- Markdown              86          581            0          564           17
 (Total)                            32235        26951         1596         3688
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Total                   980        99317        78554         8025        12738
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

這個項目裡面 Rust 的部分差不多有 32K SCoL Rust,不包含依賴體積。

::github
repo = "canmi21/seam"
align = "left"
ref = "a7f34cb47df324c84de7725296c9ef539e05b791"

這個倉庫實際上是一個有很多子項目的 monorepo,但是其中可以找出 2 個典型的例子,首先看看 Skeleton 這個包特點就很明顯,代碼量大,依賴極少,這個包會是整個項目裡面 codegen 佔比最高的,這個就會很適合 Cranelift 發揮,另外就是這個包也很乾淨,乾淨指的是純 Safe Rust。

::cargo
crate = "seam-skeleton"

反觀下面這個 CLI 包就不是很合適,相依性多倒不是什麼問題,理論上這樣更能體現效率,但是這裡有一個經典的二選一,可以在左上角看到 ring 這個相依性標記為選配了;那是因為專案本來用的是 aws-lc-rs,這兩個東西都是 Rust 中的加密演算法 Backend;但是為什麼要可選呢?那是因為 ring 是純 Rust 寫的,而另外一個是 Asm 組合語言 FFI 進來的,專為 x86 arm64 等主流 cpu 架構組合語言加速,但是敗也在此,因為 Cranelift 的魔法只局限於純 Rust,一旦你引進 Unsafe Code,or FFI C、ASM 之類的,此時實際上 Cranelift 相容性極差。

::cargo
crate = "seam-cli"

但是這也不是無解,其實可以用 cfg 選後端,Cranelift 模式下給 ring 來編譯就好了。在按照上述描述配置好 dev 和 release profile 後,就可以簡單跑一下 CLI 的編譯對比,在這個情況下,開發至少比發布快 3 倍,這還都是冷編譯,沒有增量的情況下;並且使用 O0 和關閉 LTO,dev profile 在後續的增量更新中都應該比 release 快幾十倍不止。因為 LTO 之類的魔法實際上是透過拍平各個 crate 的邊界得來的,那麼都變成一個整體後,修改一處程式碼,當前一起都要重新編譯無法被正確增量更新。

這是一張終端機視窗的螢幕截圖,視窗標題為「~/C/P/seam」,顯示名為「seam」的專案執行 `cargo build --release` 的最後階段。捲軸列出數十個依賴 crate 按順序編譯——walkdir、rand、tungstenite、sha2、toml、notify、rquickjs、clap 等——接著是專案本身的工作區成員,如 seam-codegen、seam-skeleton、seam-injector-wasm、seam-engine-wasm、seam-server、seam-server-axum、seam-cli,以及數個範例後端(demo-server-rust、github-dashboard-axum、markdown-demo-rust、i18n-demo-axum)。最後幾行確認成功:`Finished `release` profile [optimized] target(s) in 1m 05s`,接著是一個閒置的 Shell 提示字元 `canmi@xyy ~/C/P/seam (main)>`。這證明了這個包含 WASM 與 Axum 伺服器元件的多 crate Rust monorepo 已完成乾淨、完整的 Release 建置。 終端畫面截圖顯示了 Rust 工作區「seam」的 Cargo 建置日誌尾端,目錄為 `~/C/P/seam`,使用者 `canmi` 在 `main` 分支上執行 shell。數十個依賴 crate 依序編譯(如 console、reqwest、clap、tokio-tungstenite、notify、indicatif 等),接著是專案自己的 crate —— seam-codegen、seam-server-axum、seam-skeleton、seam-injector-wasm、seam-engine-wasm、seam-cli,全部為 0.5.38 版 —— 以及數個示例後端(github-dashboard-axum、i18n-demo-axum、markdown-demo-rust、demo-server-rust)。日誌以 `Finished \`dev\` profile [unoptimized + debuginfo] target(s) in 19.23s` 結束,表示整個工作區包括其示例應用程式已成功完成除錯建置。

綜合結果看下來這兩玩意其實差不多,談不上質變但是絕對能明顯感知,主要原因是 Apple Silicon 太猛了,M 系列晶片的單核效能、記憶體頻寬都強得離譜,編譯這種重計算+重 I/O 的任務剛好吃到這些優勢。如果用 Linux 跑跑通常來說差距會更大,但是如果在一台 MacBook 刷 Asahi Linux 之類的肯定是 Linux 贏。

老舊的連結器

在 Linux 上,Rust 預設使用的是 GNU ld(bfd),對,就是那個最老最慢的。單執行緒且處理符號解析與重定位都是串行掃描,專案一大就明顯拖後腿了。Linux 上可以試試 @Rui Ueyama 寫的 mold,設定起來非常簡單,只是在 .cargo/config.toml 裡加一行的事。

toml
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
這是一個條形圖,比較四個鏈接器的鏈接時間──GNU ld 2.42、GNU gold 2.38、LLVM lld 19.0.0 以及 mold 2.4.0──在三個二進制大小遞增的程式上:MySQL 8.3(0.47 GiB)、Clang 19.0(1.56 GiB)與 Chromium 124(1.35 GiB),垂直軸顯示以秒為單位的鏈接時間。GNU ld 在 MySQL 上約 11 秒,在 Clang 上約 42 秒,但在 Chromium 上沒有條形;GNU gold 在三個程式上分別約 7.5 秒、33 秒與 27 秒;而 LLVM lld 與 mold 整體保持在較低水準,lld 大約為 2、5、6 秒,mold 始終低於 2 秒。此圖表證明 mold 與 LLVM lld 在二進制規模增長時的可擴展性遠優於傳統的 GNU ld 與 gold 鏈接器,後者的鏈接時間隨程式變大而顯著上升。

提升還是很明顯的,但很遺憾 macOS 上只有 sold 這個商業專案,不過好消息是 macOS 上的 lld 或 Apple 自帶的 ld64 本身已經不慢了,特別是 Xcode 15 之後那個新連結器,速度提升超級明顯,只不過這玩意唯一的缺點就是 macOS 無人值守 CI 和更新後每次都要 sudo Accept 一個條款,導致我好幾次 CI 定時任務掛掉。

MUSL

我本人是一個 MUSL 廚,非常討厭 GNU glibc,但是開發我還是推薦你使用 x86_64-unknown-linux-gnu。為什麼呢,因為快。MUSL 最大的賣點是能生成完全靜態連結的二進位檔——不依賴系統上的任何動態庫,編譯出來的東西拷到任何 Linux 機器上就能跑,特別適合容器和嵌入式。但是慢也慢在靜態連結,因為連結器需要把所有東西都打包進去,連結階段的工作量比動態連結大不少。

不過好消息是如果你用 macOS 的話,那就完全不用擔心了,aarch64-apple-darwin 就是目前唯一的選擇了,macOS 從設計上就不支援完全靜態連結,從編譯速度的角度來說,這反而是好事。所以 dev profile 在 linux 上 gnulibc,macOS 上 libSystem,release profile 在 macOS 和 linux 上都可以 musl,因為 macOS 預設選擇 linker 和 ar 的問題,唯一值得注意的就是可能需要設定一下 .cargo/config.toml

toml
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"
ar = "x86_64-linux-musl-ar"

順帶一提,千萬別使用 zig,nightly Rust 上的 zbuild 目前問題相當嚴重。需要交叉編譯且 host 為 Linux x86 時,建議使用 cross。配合 Docker 進行編譯非常好,環境完整。如果你偏好 glibc,這也是不錯的選擇,因為 glibc 只能向後相容。一般會依照 Debian 大版本(例如 Debian‑2)的 glibc 版本來編譯,否則太新的 glibc 會讓大量機器跑不動;這並不是因為用了新功能,而純粹是透過判斷字串版本來讓你煩惱。host 為 mac 時請使用 rustup 原生 target + cargo build --release .

UPX 是個壞東西

還有一個值得討論的點就是關於 UPX 的使用;我知道我說我喜歡 musl,但是又追求小體積實際上很衝突,聽起來沒道理...

                 ooooo     ooo  ooooooooo.  ooooooo  ooooo
                 `888'     `8'  `888   `Y88. `8888    d8'
                  888       8    888   .d88'   Y888..8P
                  888       8    888ooo88P'     `8888'
                  888       8    888           .8PY888.
                  `88.    .8'    888          d8'  `888b
                    `YbodP'     o888o       o888o  o88888o


                    The Ultimate Packer for eXecutables
   Copyright (c) 1996-2026 Markus Oberhumer, Laszlo Molnar & John Reiser
                           https://upx.github.io

但是實際上 upx 的存在讓這個問題得到了解決,UPX 的原理實際上是把你的 BIN 壓縮後包上一個解壓 stub,執行時先在記憶體裡解壓再執行,這樣就可以把二進位體積壓縮到原來的 30%~50% 左右,體積優勢就會非常明顯。但也正是因為這樣,UPX + GNU glibc 動態鏈結就會有大問題,因為 glibc 動態鏈結的二進位檔案裡有一些特殊的 section 和動態載入機制,UPX 壓縮之後可能會破壞這些結構,導致執行時找不到動態程式庫 segment fault;但是反觀 musl,這個就非常適合 UPX,本來就是全靜態,內部穩定,UPX 後解壓和壓縮都很乾淨。所以 musl 的 release CI 其實可以多加一步 UPX 是非常加分的。

另外就是 UPX 也不能濫用,UPX 只適合長任務或低頻任務。長任務是因為 UPX 啟動時需要在記憶體中解壓縮一次,這個過程雖然是全自動的,但需要一點冷啟動時間,這個影響雖然微小,但在高頻路徑上還是很致命。所以長任務啟動一次並在背景執行就很適合這個;但我並不推薦無腦地給 Docker 映像檔這種長服務的封包塞 UPX,因為實際上這是用記憶體換取磁碟空間的行為,屬於血虧,而且容器 Layer 的壓縮實際上跟 UPX 是一樣的,能減少分發時的下載體積。反而推薦的環境是某些 CLI 的用途:節省超多磁碟空間,並且啟動解壓縮時間幾乎無感,而且 musl 天然也很適合做 CLI。不然我就要點名批評 AUR 助手了——Go 寫的那個玩意兒卻沒有靜態連結,導致我曾經遇到過 ArchLinux 上的 yay 壞掉了、找不到動態函式庫,本身又是系統套件管理器,無解只能另外用 LiveISO 啟動最後 chroot 救磚。

總結

最後這些好用的配置雖然可以幫助你快速區分 dev 和 release profile,在專案代碼上來後只會越來越明顯,但是我還是建議你多寫一個 CI 來跑 Rust Stable LLVM Build,最好再加上你的各種測試一起跑,這樣本地 nightly 的 rust 也不用切換了,Edge Case 也坑不到你,push 上去有問題就被 CI 攔了,這樣就是目前最舒服的 DX 模式了;那這次就說到這裡了,下次有空再摸吧