Vì sao mở lại một tệp lần thứ hai gần như tức thì, trong khi lần đầu mất một nhịp chờ? Câu trả lời là page cache — một trong những tối ưu quan trọng nhất của hệ điều hành: Linux giữ các trang tệp vừa đọc trong phần RAM còn dư, để lần sau lấy thẳng từ bộ nhớ thay vì đọc lại từ đĩa. Bài này đo trực tiếp khác biệt "lần đầu từ đĩa" so với "lần hai từ cache" — và trên đường đo, tôi vấp đúng cái bẫy khiến hiệu ứng cache biến mất khỏi phép đo của chính mình.

Page cache

Page cache: RAM làm đệm cho đĩa

Đĩa (kể cả SSD) chậm hơn RAM nhiều bậc. Để giấu độ chậm đó, nhân giữ một page cache: mỗi trang tệp được đọc từ đĩa sẽ được lưu lại trong RAM, và lần truy cập sau lấy thẳng từ đây. Cache này trong suốt (chương trình không cần biết gì), tự động (không phải bật), chia sẻ (mọi tiến trình đọc cùng tệp đều dùng chung một bản trong cache), và cơ hội — nó dùng RAM đang rảnh, và nhường lại khi ứng dụng cần thêm bộ nhớ. Bạn xem được lượng RAM đang làm cache ở dòng Cached trong /proc/meminfo.

Điều quan trọng: cả đọc lẫn ghi đều đi qua page cache. Đây chính là chi tiết sắp làm tôi đo sai.

Đo: lạnh 93 ms, ấm 18 ms

Tôi tạo một tệp 500 MB rồi đo thời gian đọc trọn nó, đồng thời theo dõi Cached. Nhưng để thấy cả cái bẫy, hãy xem lần đo đầu tiên — cách mà nhiều người (kể cả tôi) làm theo bản năng:

A) Tạo file rồi đọc ngay, KHÔNG xóa cache:
   Cached ngay sau khi tạo file: 1573 MB
   đọc lần 1: 15 ms (33 GB/s)
   đọc lần 2: 16 ms (31 GB/s)   -> gần như bằng nhau

Hai lần đọc bằng nhau và đều nhanh như RAM. Nếu dừng ở đây, tôi đã kết luận "page cache chẳng tạo khác biệt gì". Nhưng con số Cached = 1573 MB ngay sau khi tạo tệp là một manh mối: tệp 500 MB của tôi đã nằm sẵn trong cache rồi. Nên tôi làm lại cho đúng — xóa cache trước khi đọc "lần đầu":

B) echo 1 > /proc/sys/vm/drop_caches  (đẩy trang tệp ra khỏi cache):
   Cached sau drop: 359 MB       (đã tụt, tệp bị đẩy ra)
   đọc LẠNH (từ đĩa): 93 ms (5,4 GB/s)   -> Cached lại tăng lên 862 MB
   đọc ẤM  (từ cache): 18 ms (28 GB/s)

Bây giờ khác biệt hiện rõ: đọc lạnh từ đĩa mất 93 ms, đọc ấm từ cache chỉ 18 ms — nhanh gấp ~5 lần. (Với đĩa cứng quay HDD, tỉ lệ này còn lớn hơn nhiều — hàng chục tới hàng trăm lần — vì HDD chậm hơn SSD rất nhiều; ở đây tôi đo trên SSD nên chênh lệch chỉ ~5 lần, nhưng bản chất là như nhau: RAM nhanh hơn mọi loại đĩa.) Và cột Cached kể trọn câu chuyện: 1573 MB (có tệp) → 359 MB (sau khi drop, tệp bị đẩy ra) → 862 MB (sau đọc lạnh, tệp được nạp lại vào cache). Đó là page cache đang làm việc, đo được từng bước.

Một lần tôi đo hớ: ghi file đã làm nó ấm

Sai lầm của tôi ở phần A giờ đã rõ. Tôi định đo "đọc lần đầu từ đĩa", nhưng "lần đầu" của tôi không hề lạnh. Lý do: hành động ghi tệp bằng dd cũng đi qua page cache — mỗi khối ghi ra được đặt vào cache trước (rồi mới ghi xuống đĩa sau). Nên ngay khi tạo xong tệp 500 MB, toàn bộ nó đã nằm trong RAM (Cached nhảy vọt lên 1573 MB). "Lần đọc đầu" của tôi thực ra là một lần cache hit ấm sực — không có lần đọc từ đĩa nào cả, nên đương nhiên nó nhanh ngang lần hai.

Chỉ khi tôi chủ động xóa cache (drop_caches) để đẩy các trang tệp ra khỏi RAM (thấy Cached tụt từ 1573 xuống 359 MB), lần đọc kế mới thật sự phải xuống đĩa, và con số chậm thật (93 ms) mới xuất hiện. Không có bước xóa cache, tôi đo hai lần đọc ấm và tưởng đó là "đĩa so với cache".

Bài học đo lường: muốn đo một lần đọc lạnh, bạn phải chủ động làm cho nó lạnh. Đây là một trong những cái bẫy benchmark I/O phổ biến nhất — vô số phép đo "tốc độ đọc đĩa" thực ra đang đo tốc độ page cache vì quên xóa cache trước. Và nó cùng họ với những cái bẫy các bài trước đã gặp (readahead che major fault, THP gộp lỗi trang): cái đọc thấy nhanh vì một tầng đệm vô hình đang giúp bạn, và nếu không kiểm soát tầng đệm đó, bạn đo nhầm hẳn thứ mình nghĩ. Con số Cached là cái đèn báo giúp tôi phát hiện: nó không khớp với giả định "tệp còn nằm trên đĩa" của tôi.

Vì sao điều này quan trọng khi lập trình

Hệ quả đầu tiên là hiểu vì sao "chạy lại thì nhanh hơn", và tận dụng nó. Lần khởi động đầu của một ứng dụng, lần đọc đầu một tệp cấu hình, lần truy vấn đầu chạm một bảng — tất cả có thể chậm vì dữ liệu còn trên đĩa; lần sau nhanh vì đã vào cache. Đây là lý do người ta làm ấm cache (cache warming) trước khi mở dịch vụ cho người dùng: đọc trước các tệp/dữ liệu nóng để lần truy cập thật của khách hàng là cache hit. Và là lý do đo hiệu năng phải nói rõ "cold" hay "warm" — hai con số khác nhau cả 5 lần.

Hệ quả thứ hai là RAM "rảnh" thực ra đang làm việc, đừng hoảng khi thấy nó gần đầy. Trên Linux, free -m thường cho thấy phần lớn RAM "used" hoặc nằm ở cột buff/cache — đó là page cache, không phải rò rỉ bộ nhớ. Nó cơ hội: nhân sẽ tự nhường lại cache cho ứng dụng ngay khi cần, nên "RAM đầy vì cache" là dấu hiệu hệ thống đang dùng bộ nhớ hiệu quả, không phải sắp hết. Con số cần lo là bộ nhớ khả dụng (MemAvailable), không phải MemFree.

Hệ quả thứ ba là một nguyên tắc benchmark: luôn kiểm soát trạng thái cache khi đo I/O. Con số mang theo: đọc lạnh từ đĩa (93 ms) chậm gấp ~5 lần đọc ấm từ cache (18 ms) cho cùng một tệp 500 MB; và cả ghi lẫn đọc đều nạp tệp vào page cache — nên phải drop_caches mới đo được một lần đọc lạnh thật, kẻo bạn đo hai lần đọc ấm mà tưởng là đĩa-so-với-cache. Page cache là lý do máy tính có vẻ nhanh hơn khả năng thật của đĩa; đo nó đúng là luôn hỏi: dữ liệu đang ở RAM hay ở đĩa?

Thử ba mươi giây

Trên máy Linux, chọn một tệp lớn ít khi đọc, rồi đo hai lần: echo 1 | sudo tee /proc/sys/vm/drop_caches để xóa cache, sau đó time cat tệp-lớn > /dev/null (lần lạnh), rồi chạy time cat tệp-lớn > /dev/null lần nữa ngay lập tức (lần ấm). Bạn sẽ thấy lần hai nhanh hơn hẳn — đó là page cache. Trong lúc đó, chạy free -m và nhìn cột buff/cache: nó tăng lên đúng bằng cỡ tệp sau lần đọc đầu, vì tệp vừa được nạp vào cache. Đó là toàn bộ điều bài này đo — độ chênh giữa đĩa và RAM, và cái đệm vô hình đang lặng lẽ thu hẹp nó.