Danh sách bài viết

Bài 22: Profile dev vs release — Debug Build Và Optimized Build

Bài 22 của series Rust Cơ Bản — đi sâu vào Cargo profile. Bạn sẽ hiểu profile là preset compile flag cho rustc, so sánh chi tiết profile dev (compile nhanh, debug đầy đủ) và release (tối ưu tối đa cho production), từng field quan trọng (opt-level, lto, codegen-units, panic, strip, debug-assertions, overflow-checks), cách override per-dependency với [profile.<name>.package.<crate>] và tạo custom profile extend từ release để dùng riêng cho production / CI smoke. Cuối bài đo binary thực tế: 5 MB rút còn 600 KB sau khi bật release + strip + lto = "thin".

09/06/2026
12 phút đọc
2 lượt xem
1

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 devrelease: 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ật opt-level = 1 ở dev để chạy thử nhanh hơn, khi nào release cần lto = "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ằng cargo 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.

2

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 qua cargo build --release, cargo run --release. Tối ưu tốc độ runtime tối đa, đánh đổi thời gian compile.
  • test — dùng khi cargo test. Mặc định inherit từ dev.
  • bench — dùng khi cargo 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 testcargo bench dùng chung target/debug/target/release/ tương ứng vì test/bench inherit từ dev/release.

3

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. rustc dị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 macro debug_assert!, debug_assert_eq!. Bật trong dev để bắt sai sót sớm khi test cục bộ.
  • overflow-checks = truei32::MAX + 1 sẽ 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_unwind hoạ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.

4

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 = falsedebug_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ì set overflow-checks = true thủ công.
  • lto = falseLTO 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:

Fielddev defaultrelease default
opt-level03
debugtruefalse
debug-assertionstruefalse
overflow-checkstruefalse
ltofalsefalse
codegen-units25616
panicunwindunwind
stripnonenone
incrementaltruefalse
5

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.

6

[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.

7

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/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).

8

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-checksdebug-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.

9

Đ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-app với lto = "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).

10

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>, output target/<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.
11

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Tại sao Rust dev default bật overflow-checks = true còn release thì false? Trade-off cụ thể là gì?
  2. Bạn đang viết game 2D dùng crate image để decode sprite. Chạy cargo run mỗi lần load level mất 8 giây ở bước decode. Cấu hình Cargo.toml nào sẽ giảm thời gian decode mà không tăng đáng kể compile time của code game?
  3. Khi nào bạn nên đặt panic = "abort" ở release, khi nào nên giữ unwind?
  4. Custom profile [profile.production] đặt lto = "fat" + codegen-units = 1. Chạy cargo build --release có dùng các flag này không? Vì sao?
  5. cargo bench mặc định dùng profile nào? Nếu bạn thêm [profile.bench] tự định nghĩa với opt-level = 2 thì có hợp lệ không?
Đáp án
  1. 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ành i32::MIN. Bạn có thể giữ overflow-checks = true ở release cho service nhạy về data integrity.
  2. Thêm [profile.dev.package.image] opt-level = 3 (và có thể cả png, jpeg...). Crate image sẽ đượ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 (compile image với optimize), về sau cache lại.
  3. abort phù hợp khi: CLI tool ngắn hạn, embedded, WASM, app không cần catch_unwind, muốn binary nhỏ. Giữ unwind khi: service dài chạy multi-thread cần cleanup từng thread, dùng catch_unwind ở boundary FFI hoặc thread pool, lib cần unwinding cho RAII đảm bảo (file flush, lock release).
  4. Không. cargo build --release luôn dùng profile release, không tự động lan sang profile production. Phải gõ cargo build --profile production để áp dụng các flag đó. Đây là tách biệt có chủ ý để bạn giữ profile release nhanh cho local test, dùng production cho artifact deploy.
  5. cargo bench inherit từ release. Bạn được phép override [profile.bench] với field tùy ý, vd opt-level = 2. Khi đó cargo bench sẽ dùng opt-level = 2 thay vì 3. Hợp lệ, nhưng thường không khuyến nghị vì benchmark cần đo perf tối đa.
12

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.