Có một lời gọi hệ thống nghe rất hấp dẫn với người muốn tối ưu: mlock. Nó "khóa" một vùng nhớ vào RAM, ngăn hệ điều hành đẩy các trang đó ra swap. Nhiều người nghe "khóa trong RAM" liền nghĩ "vậy truy cập vùng đó sẽ nhanh hơn — nó luôn trong bộ nhớ mà". Tôi đo ba thứ trong container gcc:13: chi phí chạm-lần-đầu một vùng có/không mlock, chi phí của chính lời gọi mlock, và tốc độ truy cập steady-state khi trang đã nằm trong RAM. Kết quả buộc tôi tách bạch cái mlock thật sự làm với cái người ta tưởng nó làm.
mlock làm gì, và bộ nhớ ảo lười thế nào
Nhớ lại phần 6 về page fault: khi bạn mmap một vùng, nhân không cấp trang vật lý ngay — nó cấp lười, và trang thật chỉ được nạp vào lần đầu bạn chạm tới (một page fault). Đó là lý do lần chạm đầu tiên đắt hơn các lần sau. mlock(addr, len) bảo nhân hai điều: (1) nạp ngay mọi trang của vùng đó vào RAM (prefault), và (2) ghim chúng lại — cấm swap ra đĩa cho tới khi munlock.
Điều quan trọng cần tách bạch: mlock không làm bộ nhớ chạy nhanh hơn. RAM là RAM; một trang đã nằm trong RAM thì đọc/ghi nó tốn đúng bằng nhau, dù có bị mlock hay không. Cái mlock đổi là (a) khi nào trang được nạp (ngay, thay vì lười), và (b) đảm bảo trang không bao giờ rời RAM. Tôi đo để thấy rõ cả hai — và để bác cái niềm tin "nhanh hơn".
Đo: prefault thật, nhưng truy cập không nhanh hơn
Đầu tiên, chạm-lần-đầu một vùng mmap mới 16 MB (4096 trang), có và không mlock trước:
CHẠM-LẦN-ĐẦU (16 MB = 4096 trang):
KHÔNG mlock : 68,6 ns/trang (mỗi trang trả một page fault)
chi phí mlock: 50 ns/trang (mlock phải fault vào hết mọi trang -> 204 µs tổng)
CÓ mlock : 8,3 ns/trang (đã prefault, không còn fault -> nhanh 8×)
Không mlock, mỗi trang chạm lần đầu trả một page fault ~68,6 ns. Sau khi mlock, chạm-lần-đầu chỉ còn 8,3 ns/trang — nhanh 8 lần, vì trang đã resident, không còn fault. Đây là lợi ích prefault thật, giống hệt MAP_POPULATE mà phần 7 đã gặp.
Nhưng nhìn dòng giữa: bản thân mlock tốn ~50 ns/trang (204 µs cho 16 MB) — vì nó phải fault vào từng trang ngay lúc gọi. Nghĩa là mlock không xóa phí page fault; nó chỉ dời phí đó từ lười (rải ra khi bạn chạm) sang sớm (dồn vào lời gọi mlock). Tổng công gần như không đổi: mlock (50) + chạm (8,3) ≈ 58 ns/trang, xấp xỉ 68,6 ns của chạm-lười-có-fault. Cái bạn được không phải "ít việc hơn", mà là "biết trước khi nào trả phí" — một khác biệt quan trọng cho độ trễ ổn định.
Giờ đến câu hỏi cốt lõi — steady-state, khi trang đã nằm trong RAM, đọc lặp lại có nhanh hơn nhờ mlock không?
STEADY-STATE (trang đã resident, đọc mỗi 64 byte):
KHÔNG mlock : 0,66 ns/dòng-cache
CÓ mlock : 0,59 ns/dòng-cache
-> Y HỆT (chênh nằm trong nhiễu đo)
Câu trả lời rõ ràng: không. Truy cập một trang đã resident tốn ~0,6 ns/dòng-cache dù có mlock hay không — chênh lệch 0,66 so 0,59 nằm trong nhiễu đo. mlock không làm RAM nhanh hơn, không cải thiện thông lượng đọc/ghi. Một khi trang đã ở trong bộ nhớ, mlock chẳng thêm được gì cho tốc độ.
Một lần tôi đo hớ: "mlock làm truy cập nhanh hơn"
Tôi vào đo với đúng niềm tin trực giác: "khóa trong RAM nghĩa là truy cập nhanh hơn". Đo phá tan: steady-state y hệt. mlock không phải một nút "tăng tốc bộ nhớ". Cái nó thật sự cho là hai thứ khác: prefault (bỏ page fault lần đầu — nhưng chỉ bằng cách dời phí sang lúc gọi, không xóa) và đảm bảo không swap.
Vế thứ hai — đảm bảo — mới là lý do tồn tại thật của mlock, và nó là một ngữ nghĩa chứ không phải một con số tốc độ. Trang bị ghim không bao giờ bị đẩy ra đĩa, nên không bao giờ có major fault (fault phải đọc lại từ swap — đắt hàng nghìn lần một minor fault). Điều đó cho hai thứ mà không phép "tăng tốc" nào cho được: độ trễ ổn định (một vòng lặp real-time không bị một cú swap-in bất chợt làm khựng cả mili-giây), và bảo mật (một khóa mã hoá giữ trong vùng mlock không bao giờ bị ghi ra đĩa/swap — nơi nó có thể bị đọc trộm sau này; đây là lý do các thư viện mật mã mlock vùng chứa khóa).
Cần thành thật về giới hạn phép đo: swap không quan sát được trong container này (không có swap cấu hình), nên tôi không trực tiếp đo được cảnh mlock cứu một trang khỏi bị swap. Tôi đo được phần prefault và steady-state, và suy ra phần đảm bảo từ ngữ nghĩa của nó. Bài học đo lường: mlock không tăng tốc bộ nhớ đã resident; giá trị của nó là prefault (dời phí fault sang sớm) và đảm bảo không swap (độ trễ tất định + an toàn khóa), không phải tốc độ truy cập. Nếu tôi tin "mlock cho RAM nhanh hơn" mà rải nó khắp nơi để tối ưu, tôi đã ghim vô ích hàng loạt trang (ăn RAM, hại cả hệ) mà không được một nano-giây nào.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên: đừng dùng mlock để "tăng tốc" — dùng nó để đảm bảo. Nếu bạn có một vòng lặp nhạy độ trễ (xử lý âm thanh, giao dịch, điều khiển real-time) không được phép khựng vì một cú swap-in, mlock vùng làm việc của nó cho độ trễ tất định. Nếu bạn giữ bí mật (khóa riêng, mật khẩu) trong bộ nhớ, mlock để nó không rơi ra swap. Đó là hai ca dùng đúng — cả hai về bảo đảm, không về tốc độ.
Hệ quả thứ hai: nếu chỉ cần bỏ page fault lần đầu, cân nhắc MAP_POPULATE thay vì mlock. Cả hai prefault; nhưng mlock còn ghim (tốn hạn mức RLIMIT_MEMLOCK, có thể cần quyền, và giữ RAM không cho hệ dùng lại). Ghim quá nhiều trang là ép hệ điều hành mất chỗ xoay xở, hại cả máy. Chỉ ghim đúng cái cần bảo đảm, và nhớ munlock khi xong.
Hệ quả thứ ba là tinh thần đo lường: phân biệt "khi nào trả phí" với "tổng phí", và "đảm bảo" với "tốc độ". Con số mang theo: mlock KHÔNG làm truy cập bộ nhớ nhanh hơn — trang đã resident đọc y hệt (~0,6 ns/dòng-cache dù có mlock hay không). mlock chỉ (1) PREFAULT: bỏ page fault lần đầu (8,3 vs 68,6 ns, 8×) NHƯNG bằng cách dời phí fault vào chính lời gọi mlock (~50 ns/trang), không xóa; và (2) ĐẢM BẢO không swap -> không major fault -> độ trễ ổn định (real-time) + bảo mật (khóa không rơi ra đĩa). Cần RLIMIT_MEMLOCK; ghim nhiều hại hệ. "Khóa trong RAM" là một lời hứa về tính chắc chắn, không phải một cú tăng tốc.
Thử ba mươi giây
Nghĩ về lần cuối bạn (hay ai đó) định dùng mlock "cho nhanh": mục tiêu thật là gì — tốc độ, hay sự chắc chắn? Nếu là tốc độ, phép đo trên nói thẳng: trang đã trong RAM không nhanh thêm vì bị khóa, bạn sẽ ghim RAM vô ích. Nếu là đảm bảo không bị khựng vì swap (một vòng real-time) hoặc không để bí mật rơi ra đĩa (một khóa mã), thì mlock đúng là công cụ — nhưng vì lý do ngữ nghĩa, không phải hiệu năng. Ba mươi giây tự hỏi "mình cần bảo đảm hay cần tốc độ?" đó tránh cho bạn cả một lớp tối ưu sai: mlock là để hứa rằng trang sẽ luôn ở đó, không phải để làm nó chạy nhanh hơn khi đã ở đó.