Mục lục
- Mục Tiêu Bài Học
- Profile Là Gì
- Profile dev — Build Nhanh Để Debug
- Profile release — Build Tối Ưu Cho Production
- Customize [profile.dev] Và [profile.release]
- [profile.<name>.package.<crate>] Per-Dependency
- Custom Profile (Rust 1.57+)
- Field Quan Trọng Giải Thích Chi Tiết
- Đo Hiệu Quả Optimization
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu profile Cargo là preset compile flag truyền xuống rustc, biết 4 profile built-in (
dev,release,test,bench). - Đọc được bảng so sánh default từng field giữa profile
devvàrelease:opt-level,debug,debug-assertions,overflow-checks,lto,codegen-units,panic,strip,incremental. - Override profile trong
Cargo.toml, biết khi nào nên bậtopt-level = 1ở dev để chạy thử nhanh hơn, khi nào release cầnlto = "thin"+codegen-units = 1+strip = true. - Bật optimization riêng cho 1 crate dependency (vd
image,regex) bằng[profile.dev.package.<crate>]mà không ảnh hưởng tốc độ build code của bạn. - Tạo custom profile
[profile.production]extend từreleaseđể build artifact cho deploy, chạy bằngcargo build --profile production. - Đo binary trước/sau optimization và biết các trade-off (compile time vs runtime perf vs binary size).
B20: Cargo.toml Anatomy đã giới thiệu nhanh section [profile.dev] / [profile.release]. Bài 22 đi sâu từng field, từng trade-off, kèm số đo thực tế. Đọc tiếp B23: Cargo.lock để hoàn thiện Group 3.
Profile Là Gì
Cargo profile là một preset (bộ cài đặt sẵn) gồm các flag mà Cargo truyền xuống rustc khi compile. Mỗi profile quyết định: code có optimize hay không, có giữ debug symbol không, có bật assert/overflow check không, có làm LTO không, dùng bao nhiêu codegen-unit, panic theo unwind hay abort, có strip binary không, có incremental compile không. Toàn bộ tham số đó gom thành 1 tên gọi để khi build chỉ cần chọn profile, khỏi gõ tay từng flag.
Cargo có sẵn 4 profile built-in:
dev— default khi gõcargo build,cargo run,cargo check. Tối ưu cho tốc độ compile và trải nghiệm debug, không tối ưu tốc độ runtime.release— kích hoạt quacargo build --release,cargo run --release. Tối ưu tốc độ runtime tối đa, đánh đổi thời gian compile.test— dùng khicargo test. Mặc định inherit từdev.bench— dùng khicargo bench. Mặc định inherit từrelease(vì benchmark cần đo perf trên binary đã optimize).
Từ Rust 1.57 (tháng 12/2021) trở đi, bạn được phép tạo custom profile tên tùy ý, bắt buộc dùng key inherits để chỉ định profile gốc kế thừa. Ví dụ tạo profile production extend từ release rồi bật thêm LTO fat, strip symbols. Cách dùng: cargo build --profile production, output ra target/production/.
Cargo lưu output mỗi profile vào thư mục riêng trong target/: target/debug/, target/release/, target/<custom-name>/. Riêng cargo test và cargo bench dùng chung target/debug/ và target/release/ tương ứng vì test/bench inherit từ dev/release.
Profile dev — Build Nhanh Để Debug
Profile dev được thiết kế cho vòng lặp edit → compile → run → debug hằng ngày. Mặc định nó ưu tiên thời gian compile thấp và khả năng debug rõ ràng, chấp nhận binary lớn và chạy chậm.
Default của dev:
opt-level = 0— không optimize.rustcdịch trực tiếp Rust → LLVM IR → machine code mà không gọi pass tối ưu nào. Kết quả: compile rất nhanh, code chạy chậm có thể gấp 10-50 lần so với release.debug = true— sinh full debug info (DWARF trên Linux/macOS, PDB trên Windows).gdb,lldb, debugger của VS Code (CodeLLDB) đọc được symbol, set breakpoint, inspect biến đúng tên gốc.debug-assertions = true— bật macrodebug_assert!,debug_assert_eq!. Bật trong dev để bắt sai sót sớm khi test cục bộ.overflow-checks = true—i32::MAX + 1sẽ panic ngay ("attempt to add with overflow") thay vì silent wrap. Bắt được integer overflow ở dev cực kỳ quan trọng vì đây là một class bug phổ biến.lto = false,codegen-units = 256— chia code thành 256 đơn vị parallel codegen để tận dụng nhiều core CPU, không link-time optimize. Compile nhanh nhất có thể.panic = "unwind"— panic unwind stack, chạy destructor.catch_unwindhoạt động.incremental = true— Cargo lưu cache đơn vị compile chưa đổi, chỉnh 1 file chỉ recompile crate liên quan.
Hệ quả thực tế: binary dev thường lớn hơn release 3-5 lần do giữ symbol và inline ít. Nhưng đó là cái giá đáng trả để debugger hoạt động đúng và bạn iterate nhanh.
Profile release — Build Tối Ưu Cho Production
Profile release được thiết kế cho artifact deploy production: chạy nhanh nhất có thể, binary gọn, chấp nhận thời gian compile dài. Kích hoạt bằng:
cargo build --release
cargo run --release
Default của release:
opt-level = 3— bật mọi pass tối ưu LLVM: inlining aggressive, loop unrolling, vectorization (SIMD), dead code elimination, constant folding. Code chạy nhanh hơn dev khoảng 10-50 lần tùy workload (numeric/parser tăng nhiều, I/O-bound tăng ít).debug = false— không sinh debug info. Binary nhỏ hơn rõ rệt, nhưng stack trace khi panic ít thông tin.debug-assertions = false—debug_assert!bị skip hoàn toàn (zero cost).overflow-checks = false— integer overflow silent wrap (theo two's complement). Đây là trade-off perf vs safety; muốn giữ check ở release thì setoverflow-checks = truethủ công.lto = false— LTO không bật mặc định. Nhiều người tưởng release tự bật LTO, không phải. Muốn có LTO phải override.codegen-units = 16— chia 16 đơn vị parallel. Ít hơn dev (256) nên optimization xuyên đơn vị tốt hơn, nhưng compile lâu hơn.panic = "unwind",strip = "none",incremental = false.
Thời gian compile release thường lâu gấp 2-5 lần dev cho cùng codebase. Trên project nhỏ vài nghìn dòng có thể 30 giây; project trung bình vài chục crate dep dễ vài phút. Đó là lý do bạn không dùng release cho development hằng ngày, chỉ build release khi cần benchmark, smoke test perf, hoặc deploy.
Bảng tổng kết default từng field:
| Field | dev default | release default |
|---|---|---|
opt-level | 0 | 3 |
debug | true | false |
debug-assertions | true | false |
overflow-checks | true | false |
lto | false | false |
codegen-units | 256 | 16 |
panic | unwind | unwind |
strip | none | none |
incremental | true | false |
Customize [profile.dev] Và [profile.release] Trong Cargo.toml
Override default profile bằng cách khai báo section [profile.<name>] trong Cargo.toml. Mọi field không khai báo sẽ giữ default ở mục 3-4.
Ví dụ điển hình ở dev: bật opt-level = 1 cho tất cả dependency (nhanh hơn rõ rệt khi run app, vẫn giữ debug cho code của bạn):
[profile.dev]
opt-level = 0 # giữ default cho code của bạn
debug = true
debug-assertions = true
overflow-checks = true
# Tăng tốc dep ở dev mà không ảnh hưởng debugability code chính
[profile.dev.package."*"]
opt-level = 1
Wildcard "*" match mọi dependency. Idiom này phổ biến trong project có dep nặng như image, serde_json, regex, tokio — chạy ở dev với opt-level=0 sẽ chậm gấp 10 lần, mà bạn lại hiếm khi debug bên trong các crate này.
Ví dụ release tối ưu hoàn chỉnh cho production CLI / service nhỏ:
[profile.release]
opt-level = 3
lto = "thin" # link-time optimization xuyên crate, compile chậm thêm ~30-50%
codegen-units = 1 # 1 đơn vị duy nhất → LLVM thấy toàn bộ chương trình, optimize tối đa
strip = true # strip symbol khỏi binary, giảm size 30-70%
panic = "abort" # bỏ unwinding code, giảm size thêm ~5-15%
incremental = false # production không cần incremental
Lưu ý panic = "abort" đánh đổi: panic không unwind, không chạy destructor (file không flush, lock không release). Nếu app của bạn không dùng catch_unwind và toàn bộ cleanup được lo bởi OS process exit (CLI tool, short-lived service), abort là OK. Service dài chạy nhiều thread cần unwind để cleanup từng thread thì giữ unwind.
Một số field tăng compile time đáng kể (codegen-units = 1, lto = "fat") — bật ở release chấp nhận build chậm 2-5 phút, không bật ở dev.
[profile.<name>.package.<crate>] — Per-Dependency Optimization
Cargo cho phép override profile riêng cho từng crate dependency, không ảnh hưởng các crate còn lại. Cú pháp [profile.<name>.package.<crate-name>].
Use case kinh điển: bạn đang viết game / image processing / video encoder. Code của bạn ít, đang debug nên cần opt-level=0. Nhưng dep nặng như image, nalgebra, wgpu, regex nếu chạy ở opt-level=0 sẽ chậm khủng khiếp (decode 1 file PNG có thể từ 2 giây thành 200 ms khi bật opt-level=3). Giải pháp:
[profile.dev]
opt-level = 0
# Bật opt-level=3 chỉ cho crate image
[profile.dev.package.image]
opt-level = 3
# Bật opt-level=3 cho regex và nalgebra
[profile.dev.package.regex]
opt-level = 3
[profile.dev.package.nalgebra]
opt-level = 3
Lần đầu compile, các crate đó sẽ compile chậm hơn (vì bị optimize), nhưng đã cache lại. Lần sau code của bạn vẫn compile nhanh ở opt-level=0, chỉ recompile khi sửa code của bạn, không động đến cache của dep. Đây là một trong những tinh chỉnh hiệu quả nhất khi project có dep heavy.
Wildcard "*" như ví dụ ở mục 5 ([profile.dev.package."*"]) áp dụng cho mọi dep — chọn cách này khi không biết chính xác dep nào nặng. Specific crate override sẽ ưu tiên hơn wildcard.
Lưu ý: per-dependency override chỉ ảnh hưởng opt-level, debug, một số field cụ thể (xem doc Cargo). Không thể override lto hay codegen-units cho 1 crate riêng — đây là tham số toàn build.
Custom Profile (Rust 1.57+)
Khi cần nhiều variant build (release thường để dev test, release deploy với LTO fat, profile smoke test cho CI), bạn tạo custom profile. Bắt buộc có inherits = "<profile-gốc>".
[profile.release]
opt-level = 3
lto = "thin"
codegen-units = 1
strip = true
# Custom profile cho artifact deploy production
[profile.production]
inherits = "release"
lto = "fat" # LTO toàn chương trình, mạnh hơn "thin"
strip = "symbols" # strip cả symbol table, binary nhỏ nhất
debug = false
# Custom profile cho CI smoke test: compile nhanh hơn release nhưng vẫn có optimization
[profile.ci-smoke]
inherits = "dev"
opt-level = 1
debug = false
incremental = false
Build và chạy bằng flag --profile:
cargo build --profile production
cargo run --profile production
cargo build --profile ci-smoke
Output ra target/production/ và target/ci-smoke/ tương ứng. Lưu ý: cargo build --release là shortcut riêng cho profile release (cú pháp cũ vẫn giữ vì backward compat), không có shortcut --production; bạn luôn phải gõ --profile production.
Custom profile rất tiện cho monorepo nhiều binary: 1 profile production cho artifact deploy, 1 profile perf-test cho local benchmark nhanh hơn release tiêu chuẩn (vd bật lto=false để compile nhanh nhưng giữ opt-level=3), 1 profile release-small cho embedded / WASM cần binary tối thiểu (opt-level = "z", panic = "abort", strip = true).
Field Quan Trọng Giải Thích Chi Tiết
opt-level: 0 (không optimize), 1 (basic), 2 (tốt), 3 (mạnh nhất, default release), "s" (optimize cho size, vừa phải), "z" (size nhỏ nhất, tắt loop vectorize). Cho embedded / WASM thường dùng "z" hoặc "s".
lto (Link Time Optimization): cho phép LLVM optimize xuyên crate boundary lúc link. false = không LTO. "thin" = thin LTO, chậm thêm ~30-50% compile, tăng perf 5-20%, dùng được trong production. "fat" hoặc true = full LTO, chậm thêm 2-5 lần compile, tăng perf 10-30%, binary nhỏ hơn nữa.
codegen-units: số đơn vị parallel mà rustc chia crate ra để codegen. Nhiều unit = compile nhanh hơn (parallel) nhưng optimization tệ hơn (LLVM không thấy toàn bộ). 1 = single unit = optimize tối đa, compile chậm nhất. Default release 16, default dev 256.
panic: "unwind" (default) chạy destructor lúc panic, catch_unwind hoạt động, có cost code unwinding (~5-15% size). "abort" = panic abort process ngay, không cleanup, binary nhỏ hơn, thường dùng cho CLI/embedded. Lưu ý: nếu lib bạn dùng yêu cầu unwind (vd panic-catching) thì không set abort được.
strip: gỡ symbol khỏi binary để giảm size. "none" (default), "debuginfo" (chỉ strip debug info, giữ symbol name), "symbols" hoặc true (strip cả symbol table, nhỏ nhất). Strip không ảnh hưởng tốc độ runtime, chỉ ảnh hưởng size và khả năng debug post-mortem.
debug: true/2 (full DWARF), 1 (line-info only, dùng cho production khi muốn stack trace có line number nhưng vẫn nhỏ), false/0 (không debug info).
incremental: dev default true để rebuild nhanh; release default false vì rebuild release hiếm và metadata incremental khá nặng. Không nên bật incremental trong production CI build (artifact phải reproducible).
overflow-checks và debug-assertions: dev true, release false. Một số người chọn giữ overflow-checks = true ở release cho service nhạy về tài chính/an toàn — chấp nhận mất ~5% perf để bắt overflow ngay tại chỗ thay vì silent corrupt data.
Đo Hiệu Quả Optimization
Lấy ví dụ một CLI nhỏ dùng clap, serde_json, reqwest (không TLS để cho đơn giản) — sau khi build với 3 profile, so sánh size binary trên Linux x86_64:
ls -lh target/debug/my-app target/release/my-app target/production/my-app
Kết quả tham khảo (project thực tế thường rơi vào range này, không phải con số tuyệt đối):
target/debug/my-app— khoảng 18-25 MB (debug symbol đầy đủ, không optimize, mọi monomorphization được giữ).target/release/my-app— khoảng 4-6 MB (opt-level=3, codegen-units=16, không strip).target/production/my-appvớilto = "fat"+codegen-units = 1+strip = "symbols"+panic = "abort"— khoảng 500-700 KB.
Một con số tham khảo phổ biến: 5 MB release → 600 KB sau strip + LTO + abort, tức giảm gần 90% size. Đối với CLI tool phân phối qua cargo install hay Docker image, đây là khác biệt đáng kể.
Về thời gian compile (clean build, Apple M2):
- dev: ~12 giây.
- release default: ~35 giây.
- release + lto="thin": ~55 giây.
- production (lto="fat", codegen-units=1): ~95 giây.
Về runtime, đo bằng cargo bench hoặc hyperfine: bài toán parse JSON 100 MB, dev mất 14 giây, release mất 1.2 giây, production mất 1.0 giây. Tỷ lệ tăng dev → release là 10-12 lần, từ release → production thêm 10-20%. Đó là lý do release/production luôn được dùng cho deploy, nhưng cũng giải thích tại sao bật LTO fat ở dev là quá đắt cho gain quá nhỏ.
Để đo thực tế trên máy bạn, kết hợp cargo build --timings (xuất HTML report compile time từng crate), hyperfine ./target/release/my-app ./target/production/my-app (so sánh runtime), và cargo bloat --release --crates (xem crate nào chiếm nhiều size binary).
Tổng Kết
- Profile = preset compile flag. 4 built-in:
dev(default),release(--release),test(inherit dev),bench(inherit release). - Custom profile có từ Rust 1.57+, bắt buộc
inherits, chạy bằng--profile <name>, outputtarget/<name>/. - dev:
opt-level=0,debug=true,debug-assertions=true,overflow-checks=true,incremental=true. Compile nhanh, debug đầy đủ, chạy chậm. - release:
opt-level=3,debug=false,debug-assertions=false,overflow-checks=false,lto=false,codegen-units=16. LTO không bật mặc định. - Optimize release tối đa: thêm
lto = "thin"(hoặc"fat"),codegen-units = 1,strip = true,panic = "abort". - Per-dependency:
[profile.dev.package.image] opt-level = 3để bật optimize cho 1 crate dep mà không ảnh hưởng code của bạn. - Số đo điển hình: release + strip + LTO + abort rút binary từ 5 MB xuống 600 KB; runtime release nhanh hơn dev 10-50 lần.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Tại sao Rust dev default bật
overflow-checks = truecòn release thìfalse? Trade-off cụ thể là gì? - Bạn đang viết game 2D dùng crate
imageđể decode sprite. Chạycargo runmỗi lần load level mất 8 giây ở bước decode. Cấu hìnhCargo.tomlnào sẽ giảm thời gian decode mà không tăng đáng kể compile time của code game? - Khi nào bạn nên đặt
panic = "abort"ở release, khi nào nên giữunwind? - Custom profile
[profile.production]đặtlto = "fat"+codegen-units = 1. Chạycargo build --releasecó dùng các flag này không? Vì sao? cargo benchmặc định dùng profile nào? Nếu bạn thêm[profile.bench]tự định nghĩa vớiopt-level = 2thì có hợp lệ không?
Đáp án
- Dev bật
overflow-checksđể bắt integer overflow ngay khi test cục bộ, panic giúp lộ bug. Release tắt vì check thêm ~5% overhead cho mọi phép cộng/trừ/nhân — không chấp nhận được trong hot path. Hệ quả:i32::MAX + 1ở release silent wrap thànhi32::MIN. Bạn có thể giữoverflow-checks = trueở release cho service nhạy về data integrity. - Thêm
[profile.dev.package.image] opt-level = 3(và có thể cảpng,jpeg...). Crateimagesẽ được compile với optimize, code game của bạn vẫn ở opt-level=0 nên compile nhanh. Lần đầu chậm hơn (compileimagevới optimize), về sau cache lại. abortphù hợp khi: CLI tool ngắn hạn, embedded, WASM, app không cầncatch_unwind, muốn binary nhỏ. Giữunwindkhi: service dài chạy multi-thread cần cleanup từng thread, dùngcatch_unwindở boundary FFI hoặc thread pool, lib cần unwinding cho RAII đảm bảo (file flush, lock release).- Không.
cargo build --releaseluôn dùng profilerelease, không tự động lan sang profileproduction. Phải gõcargo build --profile productionđể áp dụng các flag đó. Đây là tách biệt có chủ ý để bạn giữ profilereleasenhanh cho local test, dùngproductioncho artifact deploy. cargo benchinherit từrelease. Bạn được phép override[profile.bench]với field tùy ý, vdopt-level = 2. Khi đócargo benchsẽ dùngopt-level = 2thay vì 3. Hợp lệ, nhưng thường không khuyến nghị vì benchmark cần đo perf tối đa.
Bài Tiếp Theo
Bài 23: Cargo.lock — Bản Chất, Khi Nào Commit — phân tích file Cargo.lock ghi exact version graph dependency, khi nào nên commit (binary crate) vs không commit (library), cách cargo update regenerate, và xử lý conflict Cargo.lock trong pull request.
