Ai cũng biết "đọc tuần tự nhanh hơn đọc ngẫu nhiên". Nhưng vì sao? Câu trả lời quen thuộc — "vì đĩa không phải di chuyển đầu đọc" — là di sản của thời đĩa cứng quay, và trên SSD nó sai. Bài này đo trực tiếp tuần tự so với ngẫu nhiên, rồi tách biến để tìm ra thủ phạm thật: một cơ chế tên là readahead (đọc trước), thứ mà tôi suýt bỏ qua vì bám vào trực giác cũ.
Readahead: nhân đoán bạn sẽ đọc tiếp
Khi nhân thấy một tiến trình đọc các trang tệp liên tiếp nhau, nó suy ra bạn đang đọc tuần tự và sẽ cần các trang kế tiếp. Thay vì đợi bạn xin, nó nạp trước (readahead) cả một vùng lớn từ đĩa vào page cache. Đến khi bạn thật sự đọc tới đó, dữ liệu đã sẵn trong RAM — lần đọc của bạn là một cache hit nhanh thay vì một lần chờ đĩa. (Chính cơ chế này đã "che" các major fault ở bài lỗi trang: readahead nạp trước bất đồng bộ nên luồng ít khi phải chặn.)
Ngược lại, khi bạn đọc ngẫu nhiên — nhảy tới các vị trí rải rác — nhân không đoán được đâu là trang kế, nên readahead vô dụng: mỗi lần đọc là một lần chờ đĩa lấy đúng một khối. Bạn có thể chỉnh readahead: posix_fadvise() gợi ý cho nhân mẫu truy cập của bạn, hoặc blockdev --setra đổi cỡ readahead của thiết bị.
Đo: tuần tự 6701, ngẫu nhiên 295, tắt readahead 134
Tôi tạo một tệp 200 MB, đọc trọn 51.200 khối 4 KB của nó theo ba kiểu, mỗi lần đều xóa cache trước (để đọc lạnh từ đĩa), và giữ cùng số khối để chỉ so thứ tự truy cập:
| Kiểu đọc | MB/s | µs/khối | So với tuần tự |
|---|---|---|---|
| Tuần tự (readahead BẬT) | 6701 | 0,6 | 1× (nhanh) |
| Ngẫu nhiên (readahead bật) | 295 | 13,9 | 23× chậm hơn |
| Tuần tự NHƯNG readahead TẮT | 134 | 30,6 | 50× chậm hơn |
Hai dòng đầu xác nhận điều ai cũng biết: đọc tuần tự (6701 MB/s) nhanh hơn ngẫu nhiên (295 MB/s) khoảng 23 lần. Nhưng dòng thứ ba mới là chỗ tôi vấp — và nó lật ngược lời giải thích quen thuộc.
Một lần tôi đo hớ: không phải seek, mà là readahead
Trực giác đầu tiên của tôi cho khác biệt tuần-tự-so-với-ngẫu-nhiên là seek: đĩa cứng quay phải di chuyển đầu đọc cơ học tới đúng vị trí, và di chuyển ngẫu nhiên khắp đĩa tốn thời gian hơn di chuyển tuần tự. Tôi đã định viết giải thích đó vào bài. Nhưng khựng lại: phép đo này chạy trên SSD — nó không có đầu đọc cơ học nào để mà di chuyển. Seek trên SSD gần như miễn phí. Vậy nếu không phải seek, cái gì làm tuần tự nhanh hơn ngẫu nhiên tới 23 lần?
Để tìm ra, tôi tách biến: đọc tuần tự y hệt (cùng thứ tự khối như bản nhanh), nhưng tắt readahead đi bằng posix_fadvise(fd, 0, 0, POSIX_FADV_RANDOM). Kết quả: tốc độ sụp từ 6701 xuống 134 MB/s — chậm 50 lần, thậm chí còn chậm hơn cả đọc ngẫu nhiên. Cùng thứ tự đọc, không đổi một khối nào, không có cú seek nào (SSD mà), chỉ khác readahead bật hay tắt — mà chênh nhau 50 lần.
Vậy thủ phạm thật đã rõ: cái làm đọc tuần tự nhanh không phải vì không seek, mà vì readahead — nhân nạp trước dữ liệu theo lô lớn nên các lần đọc của bạn hầu hết là cache hit. Và đọc ngẫu nhiên chậm không phải vì seek (SSD seek rẻ), mà vì nó vô hiệu hóa dự đoán của readahead: nhân không biết nạp trước gì, nên mỗi khối phải lấy riêng từ đĩa.
Bài học đo lường: đừng bê nguyên trực giác của phần cứng cũ sang phần cứng mới. "Tuần tự nhanh vì đĩa không seek" đúng cho ổ cứng quay, nhưng trên SSD nguyên nhân đã đổi hoàn toàn sang một cơ chế phần mềm (readahead). Cách duy nhất để biết chắc là tách biến — giữ mọi thứ như nhau (kể cả thứ tự đọc) và chỉ đổi đúng một yếu tố (readahead) — để xem yếu tố nào thật sự gây ra khác biệt. Nếu tôi tin trực giác cũ, tôi đã dạy người đọc một nguyên nhân sai và một cách tối ưu sai.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên là thiết kế truy cập dữ liệu để thân thiện với readahead. Khi có thể, hãy đọc tuần tự — quét một tệp từ đầu tới cuối, lưu dữ liệu theo thứ tự bạn sẽ đọc lại, gom các bản ghi liên quan gần nhau. Đọc tuần tự để nhân nạp trước hộ bạn là một trong những "bữa trưa miễn phí" hiếm hoi của hiệu năng — bạn không viết thêm dòng code nào mà vẫn nhanh hơn nhiều lần, chỉ nhờ sắp xếp truy cập cho có thứ tự. Ngược lại, một mẫu truy cập ngẫu nhiên (nhảy khắp một tệp lớn, đọc các bản ghi rải rác) phá vỡ readahead và có thể chậm hàng chục lần — đây là lý do các cơ sở dữ liệu tối ưu cách bố trí dữ liệu trên đĩa (clustered index, sắp xếp theo khóa) để biến truy cập logic thành đọc tuần tự vật lý.
Hệ quả thứ hai là gợi ý cho nhân khi bạn biết mẫu truy cập. Nếu bạn thật sự đọc ngẫu nhiên (một cơ sở dữ liệu, một cache trên đĩa), posix_fadvise(POSIX_FADV_RANDOM) bảo nhân đừng phí công readahead — tránh nạp trước những trang bạn sẽ không dùng, đỡ lãng phí băng thông và bộ nhớ. Nếu bạn sắp quét tuần tự một tệp lớn, POSIX_FADV_SEQUENTIAL bảo nhân đọc trước hung hăng hơn. Nói cho nhân biết ý định của bạn thường tốt hơn để nó đoán.
Hệ quả thứ ba là một nguyên tắc chẩn đoán: cùng một trực giác có thể đúng nguyên nhân nhưng sai cơ chế khi nền tảng đổi. Con số mang theo: đọc tuần tự (6701 MB/s) nhanh hơn ngẫu nhiên (295 MB/s) ~23 lần, nhưng nguyên nhân trên SSD là READAHEAD chứ không phải seek — tắt readahead thì đọc tuần tự cũng sụp còn 134 MB/s (chậm 50 lần) dù không seek. Tuần tự nhanh vì nhân nạp trước hộ bạn; ngẫu nhiên chậm vì nó làm nhân không đoán được. Khi tối ưu I/O, hãy hỏi "mẫu truy cập của tôi có để readahead giúp được không?", chứ đừng chỉ nghĩ tới seek như thời ổ cứng quay.
Thử ba mươi giây
Trên máy Linux có fio (công cụ đo I/O), so hai lệnh: fio --name=seq --rw=read --bs=4k --size=200m --filename=/tmp/f (đọc tuần tự) và đổi --rw=randread (đọc ngẫu nhiên) — bạn sẽ thấy thông lượng tuần tự cao hơn hẳn. Không có fio? Xem cỡ readahead hiện tại của một thiết bị bằng blockdev --getra /dev/sda (đơn vị 512-byte, thường 256 = 128 KB), rồi thử blockdev --setra 0 /dev/sda để tắt và đo lại một lần quét tuần tự — nó sẽ chậm đi rõ rệt, đúng như bài này đo. Cái tăng tốc đó, mà bạn không hề viết một dòng code nào, chính là readahead đang lặng lẽ đoán trước bước đi của bạn — một món quà của nhân dành riêng cho những ai chịu đọc theo thứ tự.