Rust 1.100 vừa ra, cộng đồng lại bàn về một dự án JIT compiler cho Rust với mục tiêu giảm build time. Điều này cho thấy compile time vẫn là nỗi đau lớn nhất của Rust, dù ngôn ngữ đã trưởng thành nhiều. Team mình có một service khoảng 80k dòng Rust, chia thành 14 crate trong workspace. Có thời điểm một lần cargo build incremental sau khi sửa đúng một dòng mất 40 giây. Khi AI agent tự sửa code rồi tự chạy test liên tục thì 40 giây đó bị nhân lên hàng trăm lần mỗi ngày. Bài này tổng hợp những gì mình đã làm để đưa con số đó xuống khoảng 6 giây. Không có phép màu nào cả, chỉ là đo đúng chỗ rồi sửa đúng chỗ.
1. Đo trước, tối ưu sau
Lỗi phổ biến nhất mình thấy là đổi linker hoặc thêm cache theo cảm tính mà không biết thời gian thực sự bị tiêu vào đâu. Cargo có sẵn công cụ đo khá tốt:
# Báo cáo HTML: crate nào build lâu, crate nào chặn song song cargo build --timings open target/cargo-timings/cargo-timing.html # Xem generic nào sinh ra nhiều LLVM IR nhất cargo install cargo-llvm-lines cargo llvm-lines -p my-core --lib | head -20 # Thời gian từng pass của rustc (cần nightly) cargo +nightly rustc -p my-core -- -Z time-passes Trong báo cáo cargo-timing.html, hãy chú ý hai thứ. Thứ nhất là các thanh dài nằm trên critical path, thường là crate trung tâm mà mọi crate khác phụ thuộc vào. Thứ hai là phần màu tím (codegen) so với phần màu xanh (frontend). Nếu codegen chiếm phần lớn, vấn đề nằm ở LLVM và monomorphization. Nếu frontend chiếm phần lớn, nguyên nhân thường là macro (serde, sqlx, tokio) hoặc trait resolution phức tạp.
Để chọn đúng công cụ, cần biết mỗi giai đoạn build tương ứng với công cụ nào:
flowchart LR A[Source .rs] --> B[Frontend: parse, macro, typeck] B --> C[MIR + monomorphization] C --> D[Codegen: LLVM / Cranelift] D --> E[Linker: ld / lld / mold] E --> F[Binary] S[sccache] -.cache toàn crate.-> D W[Tách crate / bớt generic] -.giảm.-> C CL[Cranelift] -.thay LLVM.-> D M[mold / lld] -.tăng tốc.-> E 2. Linker và codegen backend: thắng nhanh nhất
Với incremental build, phần link thường chiếm tỷ lệ lớn đến bất ngờ, vì mỗi lần sửa một dòng là phải link lại toàn bộ binary. Từ Rust 1.90, target x86_64-unknown-linux-gnu đã dùng lld làm linker mặc định, nên nếu bạn đang dùng toolchain mới trên Linux thì đã được hưởng phần lớn lợi ích này. Trên các target khác, hoặc khi muốn nhanh hơn nữa, mold (bản 2.x) vẫn là lựa chọn rất đáng thử.
Bước tiếp theo là Cranelift. Đây là codegen backend viết bằng Rust, sinh code chạy chậm hơn LLVM nhưng compile nhanh hơn đáng kể. Cranelift hợp với dev build và test, không nên dùng cho release. Hiện tại nó vẫn cần nightly:
rustup toolchain install nightly rustup component add rustc-codegen-cranelift-preview --toolchain nightly Sau đó cấu hình trong .cargo/config.toml và Cargo.toml:
# .cargo/config.toml [target.x86_64-unknown-linux-gnu] linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=mold"] [unstable] codegen-backend = true # Cargo.toml (workspace root) [profile.dev] codegen-backend = "cranelift" debug = "line-tables-only" # đủ cho backtrace, nhẹ hơn debug=2 rất nhiều # Dependency ít thay đổi: tối ưu một lần, cache mãi [profile.dev.package."*"] codegen-backend = "llvm" opt-level = 2 Có một mẹo ở đây: dependency bên ngoài vẫn dùng LLVM với opt-level = 2, vì chúng chỉ build một lần rồi được cache. Còn code của mình, phần thay đổi liên tục, thì dùng Cranelift. Kết quả là test vẫn chạy đủ nhanh mà vòng sửa-build lại ngắn. Với project của mình, riêng bước này đã giảm incremental build từ 40 giây xuống khoảng 14 giây.
Lưu ý: một số crate dùng SIMD intrinsics hoặc inline asm có thể chưa tương thích với Cranelift. Nếu gặp lỗi, hãy chuyển riêng crate đó về llvm qua [profile.dev.package.ten-crate].
3. Cấu trúc workspace: tối ưu bền vững nhất
Linker và backend chỉ giúp bạn đi nhanh hơn trên con đường sẵn có. Cấu trúc crate mới quyết định con đường đó dài bao nhiêu. Có ba nguyên tắc mình áp dụng:
- Tách crate theo tần suất thay đổi. Domain types ít thay đổi thì để vào crate riêng ở đáy dependency graph. Business logic thay đổi liên tục thì để ở các crate lá. Sửa crate lá thì chỉ crate đó cùng binary phải build lại.
- Không để mọi thứ phụ thuộc vào một crate
commonkhổng lồ. Mỗi lần sửacommonlà cả workspace phải build lại. Đây là nguyên nhân chính khiến build của team mình chậm. - Giảm monomorphization. Dựa vào kết quả
cargo llvm-lines, với các hàm generic lớn, hãy tách phần thân thành một hàm non-generic bên trong:
// Trước: toàn bộ thân hàm bị nhân bản cho mỗi kiểu P pub fn load_config<P: AsRef<Path>>(path: P) -> Result<Config> { // ... 80 dòng parse ... } // Sau: chỉ wrapper mỏng là generic pub fn load_config<P: AsRef<Path>>(path: P) -> Result<Config> { fn inner(path: &Path) -> Result<Config> { // ... 80 dòng parse, chỉ compile một lần ... } inner(path.as_ref()) } Thư viện chuẩn cũng dùng đúng pattern này, ví dụ std::fs::read. Ngoài ra, hãy chạy cargo machete hoặc cargo udeps để loại bỏ dependency không dùng, và rà soát feature flags. Bật tokio với features = ["full"] trong một crate chỉ cần sync là lãng phí.
4. Cache trên CI và giữa các máy
Ở local, incremental compilation đã làm khá tốt. Trên CI, mỗi job bắt đầu từ trạng thái trống, nên cache là bắt buộc. Mình kết hợp hai lớp:
sequenceDiagram participant CI as CI Job participant RC as rust-cache (target/) participant SC as sccache (S3/GCS) participant R as rustc CI->>RC: Restore target/ theo hash Cargo.lock CI->>R: cargo build R->>SC: Hash input của crate SC-->>R: Hit: trả artifact SC-->>R: Miss: compile rồi upload CI->>RC: Save target/ (chỉ trên main) File workflow GitHub Actions rút gọn:
- uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: mozilla-actions/sccache-action@v0.0.9 - run: cargo nextest run --workspace env: RUSTC_WRAPPER: sccache SCCACHE_GHA_ENABLED: "true" CARGO_INCREMENTAL: "0" Ba chi tiết dễ bị bỏ qua:
-
CARGO_INCREMENTAL=0trên CI: incremental artifact rất lớn và gần như không có ích khi đã có sccache. -
save-ifchỉ lưu cache trênmain: nếu mọi PR đều ghi cache, giới hạn 10GB của GitHub sẽ bị dùng hết rất nhanh. -
cargo-nextest(0.9.x) chạy mỗi test trong một process riêng và song song tốt hơncargo test. Với test suite lớn, nó thường nhanh hơn 30-50%.
Kết luận
Build time của Rust không phải là thứ bắt buộc phải chịu đựng. Nếu chỉ có một buổi chiều, mình gợi ý làm theo thứ tự sau:
- Chạy
cargo build --timingsđể xác định critical path trước khi thay đổi bất cứ thứ gì. - Kiểm tra linker: trên Linux x86_64 với Rust từ 1.90 trở lên, bạn đã có lld. Trên target khác, hãy thử mold.
- Thử Cranelift cho
profile.dev, đồng thời giữ LLVM vớiopt-level = 2cho dependency. - Đặt
debug = "line-tables-only": thay đổi một dòng nhưng giảm kích thướctarget/rõ rệt. - Rà soát dependency graph: tách crate
common, xử lý các hàm generic lớn, cắt feature flags thừa. - Trên CI, dùng
rust-cache+sccache+nextestvà chỉ lưu cache trênmain.
Trong thời đại AI agent tự build và tự test hàng trăm lần mỗi ngày, mỗi giây tiết kiệm được ở vòng lặp build sẽ được nhân lên rất nhiều lần. Đầu tư vài giờ cho build time thường là khoản đầu tư có lợi nhất cho team Rust.

