Rust đo thật: từ ownership tới unsafe
Mỗi bài đo một cơ chế lõi của Rust bằng container rust thật: lỗi biên dịch của borrow checker, size_of và tối ưu bố cục, assembly của iterator, chi phí Rc/Arc, số lần cấp phát lại của Vec, vtable điều phối động, data race bị chặn khi biên dịch.
11/12 phần đã đăng
Lập trình
1
Rust đo thật — ownership và move: vì sao không cần bộ gom rác; dùng lại giá trị sau khi move là lỗi BIÊN DỊCH E0382, còn drop chạy xác định theo thứ tự ngược khai báo, quyết tại lúc biên dịch chứ không phải lúc chạy
Bài mở đầu sê-ri Rust đo thật: ownership là quy tắc mỗi giá trị có đúng một chủ, và khi hết phạm vi của chủ thì giá trị bị dọn ngay — không cần bộ gom rác, không cần free thủ công. Đo trong container rust 1.99.1: gán String cho biến khác là MOVE, dùng lại biến cũ cho ra lỗi biên dịch E0382 (chương trình không chạy nổi), move chỉ sao chép 3 word trên stack nên con trỏ heap giữ nguyên địa chỉ (0xaaaa...7d60 trước và sau), i32 là Copy nên vẫn dùng được cả hai, và hàm drop chạy đúng thứ tự ngược khai báo ngay tại cuối phạm vi — toàn bộ quyết định tại lúc biên dịch.
02/11/2026
· 8 phút đọc
2
Rust đo thật — borrow checker: mượn chia sẻ thì bất biến, &mut thì độc quyền; sửa vec khi đang duyệt hay cho &mut đụng một & đang sống đều là lỗi biên dịch E0502/E0499, còn trả tham chiếu tới biến cục bộ bị chặn ngay bằng E0515
Phần 2 sê-ri Rust đo thật: ta cần cho mượn giá trị mà không trao quyền sở hữu, và borrow checker áp đúng một luật — aliasing XOR mutability: nhiều tham chiếu chia sẻ & cùng lúc thì được, nhưng chỉ cần một &mut thì nó phải độc quyền. Đo trong container rust 1.99.1 bằng chính các lỗi biên dịch: &mut v khi một &v đang còn dùng cho E0502, hai &mut cùng lúc cho E0499, sửa vec giữa vòng for cho E0502 (chặn iterator invalidation — bug kinh điển của C++), trả tham chiếu tới biến cục bộ bị chặn bằng E0515. Nhờ non-lexical lifetime, push sau khi tham chiếu hết dùng vẫn biên dịch.
03/11/2026
· 8 phút đọc
3
Rust đo thật — bố cục bộ nhớ: trình biên dịch tự sắp lại trường để struct 15 byte dữ liệu chỉ tốn 16 byte thay vì 32 như repr(C), Option<&T> vẫn đúng 8 byte nhờ tối ưu niche, còn kiểu kích thước 0 không chiếm byte nào
Phần 3 sê-ri Rust đo thật: đo bằng size_of và align_of trong container rust 1.99.1 cách Rust xếp dữ liệu. Một struct chứa 15 byte dữ liệu thật nhưng xen kẽ u8/u64/u32: repr Rust mặc định thì trình biên dịch TỰ sắp lại trường, gói còn 16 byte; ép repr(C) giữ nguyên thứ tự khai báo thì phình lên 32 byte vì padding. Tối ưu niche: Option<&T> và Option<Box<T>> vẫn đúng 8 byte (dùng null làm None), Option<bool>=1, nhưng Option<u64>=16 vì u64 dùng hết mọi bit. Enum = biến thể lớn nhất cộng cờ phân biệt (24 byte). Kiểu kích thước 0 như () tốn 0 byte, Vec<()> không cấp phát heap.
04/11/2026
· 7 phút đọc
4
Rust đo thật — trừu tượng chi phí bằng không: chuỗi iterator filter.map.sum biên dịch ra ĐÚNG mã của vòng for viết tay (9,94 ms so với 10,44 ms, cùng 0 lệnh gọi hàm và 5 lệnh nhân), nhưng chỉ sau khi tối ưu — ở bản debug nó chậm gấp 2,1 lần
Phần 4 sê-ri Rust đo thật: trừu tượng chi phí bằng không nghĩa là iterator, closure, generic biên dịch ra mã nhanh ngang code viết tay — và đo được. Trong container rust 1.99.1, cùng bài toán tổng bình phương số chẵn trên 50 triệu phần tử: bản release, chuỗi v.iter().filter().map().sum() chạy 9,94 ms còn vòng for thủ công 10,44 ms — ngang nhau. Nhìn assembly aarch64: cả hai hàm đều 0 lệnh gọi hàm (closure bị nội tuyến sạch) và 5 lệnh nhân (mở cuộn y hệt). Nhưng bằng không chỉ đúng SAU tối ưu: ở bản debug, iterator chậm 246 ms so với 114 ms của vòng tay, gấp 2,1 lần.
05/11/2026
· 6 phút đọc
5
Rust đo thật — Box, Rc, Arc: con trỏ thông minh và cái giá của việc đếm tham chiếu; Rc::clone chỉ +1 bộ đếm (2,47 ns) còn Arc dùng phép nguyên tử nên đắt gấp 1,5 lần (3,62 ns), và trình biên dịch chặn Rc qua luồng bằng E0277
Phần 5 sê-ri Rust đo thật: ba con trỏ thông minh và chi phí thật, đo trong container rust 1.99.1. Box<T> là sở hữu duy nhất trên heap — stack chỉ giữ 1 con trỏ 8 byte dù dữ liệu 1024 byte. Rc<T> cho nhiều chủ cùng sở hữu bằng đếm tham chiếu: Rc::clone KHÔNG sao chép dữ liệu, chỉ +1 bộ đếm (strong_count 1→2→3→2→1), mọi bản cùng địa chỉ heap. Arc<T> giống Rc nhưng đếm bằng phép nguyên tử để an toàn đa luồng — đo được cái giá: 100 triệu lần clone+drop, Rc mất 2,47 ns/lần còn Arc 3,62 ns/lần, chậm gấp 1,5 lần. Và trình biên dịch chặn dùng Rc qua luồng bằng lỗi E0277, buộc chọn đúng công cụ.
06/11/2026
· 6 phút đọc
6
Rust đo thật — Vec và capacity: lớn dần bằng cách nhân đôi sức chứa (0→4→8→…→2048), nên chèn 1025 phần tử chỉ cần ~10 lần cấp phát chứ không phải 1025; ép grow-by-1 thì chậm gấp 4 lần, còn with_capacity biết trước cỡ thì nhanh nhất
Phần 6 sê-ri Rust đo thật: Vec<T> là mảng động, và cách nó lớn lên quyết định hiệu năng. Đo trong container rust 1.99.1: mỗi khi hết chỗ, Vec NHÂN ĐÔI sức chứa (cap 0→4→8→16→…→2048), nên chèn 1025 phần tử chỉ cần ~10 lần cấp phát thay vì 1025 — mỗi push thành O(1) khấu hao. Thú vị: trong 10 lần đổi capacity, bộ cấp phát chỉ thật sự DỜI vùng nhớ 2 lần, còn lại nới khối tại chỗ. So ba chiến lược chèn 200.000 phần tử: nhân đôi mặc định 0,35 ms, grow-by-1 1,41 ms — chậm gấp 4 lần, with_capacity 0,34 ms. Biết trước số phần tử thì luôn dùng with_capacity.
07/11/2026
· 6 phút đọc
7
Rust đo thật — trait: điều phối tĩnh so với động; generic đơn hình hoá nội tuyến thẳng lời gọi (0 lệnh gọi gián tiếp, 12,4 ms), còn &dyn Trait nạp bảng ảo rồi gọi gián tiếp mỗi phần tử (blr qua vtable, 15,5 ms); con trỏ béo &dyn chiếm 16 byte
Phần 7 sê-ri Rust đo thật: một trait gọi được hai cách. Điều phối TĨNH (generic <T: Trait>) được đơn hình hoá — trình biên dịch sinh mã riêng cho từng kiểu, nội tuyến thẳng phương thức. Điều phối ĐỘNG (&dyn Trait) gọi qua bảng ảo (vtable) lúc chạy. Đo trong container rust 1.99.1: &Tron là con trỏ mảnh 8 byte, &dyn Hinh là con trỏ béo 16 byte Cộng diện tích 20 triệu hình: tĩnh 12,4 ms, động 15,5 ms (1,2 lần). Assembly aarch64: bản tĩnh 0 lệnh gọi gián tiếp (nội tuyến, 6 fmul), bản động nạp ô vtable rồi blr gọi gián tiếp mỗi phần tử. Tĩnh nhanh nhưng phình mã; động gọn mã mà trả giá mỗi lời gọi.
08/11/2026
· 6 phút đọc
8
Rust đo thật — lifetime: tham chiếu sống được bao lâu; thiếu chú thích khi trả tham chiếu là lỗi E0106, dùng kết quả lâu hơn nguồn là E0597, nhưng toàn bộ lifetime bị xoá sau biên dịch nên struct giữ tham chiếu vẫn đúng 16 byte, 0 chi phí lúc chạy
Phần 8 sê-ri Rust đo thật: lifetime là cách Rust theo dõi một tham chiếu được phép sống bao lâu, chỉ tồn tại lúc biên dịch. Đo trong container rust 1.99.1 bằng các lỗi: hàm trả &str lấy từ một trong hai tham số mà không nói rõ mượn từ đâu cho E0106 missing lifetime specifier. Khi đã chú thích, dùng kết quả lâu hơn một nguồn đã bị drop cho E0597 does not live long enough — đúng ca dangling pointer bị chặn. Bằng chứng zero-cost: struct giữ một &str vẫn đúng 16 byte, bằng &str trần, vì chú thích lifetime bị xoá sau biên dịch — 0 byte, 0 lệnh, 0 chi phí lúc chạy.
09/11/2026
· 7 phút đọc
9
Rust đo thật — xử lý lỗi: Result bọc trên đường thành công gần như miễn phí (chênh 0,4% so với trả giá trị trần), còn panic tháo cuộn ngăn xếp tốn 1585 ns mỗi lần nên đắt hơn hàng nghìn lần; panic=abort còn cắt bớt vài KB mã tháo cuộn khỏi nhị phân
Phần 9 sê-ri Rust đo thật: Rust tách lỗi lường trước (Result<T,E>) với lỗi không phục hồi/bug (panic) — giá khác nhau hàng nghìn lần. Đo trong container rust 1.99.1: Result<u64,E> chiếm 16 byte, nhưng Result<(),E> chỉ 1 byte nhờ niche. Trên đường thành công, bọc kết quả vào Result rồi match gần như miễn phí: 500 triệu lần gọi, Result 383,9 ms vs u64 trần 385,6 ms — chênh 0,4%. Ngược lại, panic tháo cuộn ngăn xếp rất đắt: panic + catch_unwind tốn 1585 ns/lần, trả Err gần như 0 — đắt gấp hàng nghìn lần, không dùng panic cho luồng điều khiển. panic=abort cắt ~6 KB mã tháo cuộn khỏi nhị phân.
10/11/2026
· 6 phút đọc
10
Rust đo thật — đồng thời không sợ hãi: chia sẻ Cell qua luồng bị chặn ngay khi biên dịch (E0277, Cell không Sync), còn Arc<Mutex> cho kết quả đúng 4000 không mất cập nhật; Mutex chậm gấp 2,1 lần atomic, và chia-để-trị 4 luồng nhanh gấp 3 lần
Phần 10 sê-ri Rust đo thật: đồng thời không sợ hãi = data race bị bắt lúc biên dịch. Đo trong container rust 1.99.1: chia sẻ Arc<Cell<u64>> qua 4 luồng cho lỗi E0277 Cell cannot be shared between threads safely vì Cell không cài Sync — data race bị từ chối trước khi chương trình tồn tại. Dùng Arc<Mutex<u64>> thì chạy đúng: 4 luồng mỗi luồng +1000 cho kết quả 4000, không mất cập nhật. Bộ đếm chung 4 luồng × 2 triệu: Arc<Mutex> 193 ms, AtomicU64 91 ms — Mutex chậm gấp 2,1 lần. Chia-để-trị không chia sẻ trạng thái: tổng 200 triệu phần tử, 1 luồng 154 ms, 4 luồng 51 ms — nhanh gấp 3 lần.
11/11/2026
· 6 phút đọc
11
Rust đo thật — async/await với Tokio: 100.000 task chạy đồng thời trên 10 luồng tốn ~393 byte mỗi task, trong khi mỗi luồng hệ điều hành ngốn ~9800 byte RSS (và dành riêng 2 MB stack); Future chỉ là máy trạng thái 1–80 byte
Phần 11 sê-ri Rust đo thật: async/await cho phép hàng nghìn tác vụ đồng thời trên vài luồng, mỗi tác vụ nhẹ hơn một luồng hệ điều hành cả chục lần. Đo trong container rust 1.99.1 + tokio 1.54.1: 100.000 task async cùng sống, chạy trên runtime chỉ 10 luồng worker, tốn thêm ~38 MB — tức ~393 byte mỗi task. Để so, 10.000 luồng hệ điều hành cùng sống tốn ~95 MB RSS — ~9800 byte mỗi luồng, gấp 25 lần, mỗi luồng còn dành riêng 2 MB stack ảo. Lý do: một async fn biên dịch thành MÁY TRẠNG THÁI đúng bằng thứ nó giữ qua điểm await — Future hàm rỗng chỉ 1 byte, Future giữ mảng [u64;8] qua await 80 byte.
12/11/2026
· 6 phút đọc
Còn 1 phần nữa sẽ lần lượt được đăng.