Hình dung một nhà kho có tấm bảng ghim nhỏ ở cửa, nói cho xe nâng biết pallet nào nằm ở dãy nào — nhưng bảng chỉ có ít chốt. Với "ô nhỏ", mỗi chốt chỉ trỏ một khoang; với "ô lớn", một chốt trỏ cả một dãy. Nếu kho ít pallet, cách nào cũng đủ chốt — ô lớn chẳng giúp gì. Chỉ khi kho có hàng nghìn pallet rải khắp nơi, bảng nhỏ hết chốt và xe nâng phải chạy đi tra sổ liên tục; lúc đó "một chốt mỗi dãy" mới cứu bạn. Cái bảng ghim nhỏ đó là TLB của CPU, và "ô lớn" chính là huge page — hữu ích, nhưng đúng một điều kiện, và không miễn phí.
Ở phần TLB ta thấy huge page cắt được TLB miss: một mục TLB phủ 2 MB thay vì 4 KB, nới tầm phủ lên 512 lần. Nghe như một nút bấm "nhanh hơn" miễn phí. Nhưng có thật huge page luôn nhanh hơn không? Và nó có mặt trái nào không? Bài này đo tác động của huge page ở nhiều quy mô — từ vùng nhỏ tới vùng lớn — và đo cả cái giá của nó, trong container gcc:13 (ARM). Kết quả: huge page là một công cụ có điều kiện, không phải phép màu.
Huge page giúp khi nào?
Logic từ phần trước: huge page cắt TLB miss vì cần ít mục TLB hơn cho cùng một vùng — ít chốt hơn cho cùng lượng hàng. Nhưng điều đó chỉ có ý nghĩa khi TLB đang là nút thắt. Nếu vùng làm việc đã đủ nhỏ để vừa TLB với trang 4 KB (kho ít pallet, bảng còn thừa chốt), thì huge page chẳng cắt được gì — không có miss nào để cắt. Nếu truy cập là tuần tự, bộ dự đoán prefetch của CPU che phần lớn độ trễ, TLB miss ít quan trọng. Huge page chỉ tỏa sáng ở đúng một kịch bản: vùng lớn, truy cập ngẫu nhiên, vượt tầm phủ TLB.
Để kiểm, tôi đo cùng phép chase con trỏ ngẫu nhiên như phần trước, nhưng ở ba quy mô — nhỏ (vừa TLB), vừa, và lớn (vượt TLB) — với trang 4 KB và huge page 2 MB.
Đo: lợi ích tăng theo quy mô
chase ngẫu nhiên (ns/truy cập):
0,3 MB (vừa TLB) : 4KB 5,6 ns | huge 5,0 ns | -> 1,13×
2,1 MB : 4KB 8,0 ns | huge 5,7 ns | -> 1,40×
67 MB (vượt TLB) : 4KB 55 ns | huge 33 ns | -> 1,66×
Lợi ích của huge page tăng dần theo quy mô vùng. Ở vùng nhỏ 0,3 MB — vừa gọn trong TLB dù dùng trang 4 KB — huge page chỉ nhanh 1,13 lần, gần như không đáng kể: không có TLB miss để cắt. Chỉ khi vùng lớn tới 67 MB, vượt xa tầm phủ TLB (~8 MB với trang 4 KB), huge page mới thắng rõ 1,66 lần — vì lúc đó trang 4 KB miss liên tục còn huge page vẫn hit. Đây là bằng chứng trực tiếp: huge page không "luôn nhanh hơn", nó chỉ giúp khi TLB là nút thắt. Chú ý cả hình dạng đường cong: 1,13× → 1,40× → 1,66× — lợi ích tăng dần khi vùng lớn hơn, nên cùng một chương trình có thể không hưởng gì lúc dữ liệu nhỏ rồi hưởng rõ khi dữ liệu phình to, một lý do nữa để đo ở đúng quy mô thật của bạn.
Hai niềm tin đối nghịch, cùng sai
Tôi vào đo với niềm tin phổ biến: "bật huge page thì chương trình luôn nhanh hơn — một tinh chỉnh không mất gì". Đường cong ba quy mô đã bác bỏ vế "luôn": ở vùng nhỏ hầu như không đổi (1,13×). Nhưng còn một vế nữa tôi bỏ sót cho tới khi đo — huge page có giá. Tôi đo chi phí page fault khi chạm lần đầu, so trang 4 KB với huge page:
page fault (cấp + zero):
4KB : 385 ns/fault (zero 4 KB)
huge : 40442 ns/fault (zero cả 2 MB)
Một huge-page fault tốn ~40 µs — gấp 105 lần một 4 KB fault (385 ns). Vì mỗi lần chạm đầu vào một huge page, nhân phải cấp và zero sạch cả 2 MB một lúc — đúng cái mở nguyên một dãy kho ra dọn cùng lúc. Nghĩa là một cú chạm có thể gây một cú khựng ~40 µs — một đột biến độ trễ mà trang 4 KB không có (nó rải chi phí thành nhiều fault nhỏ 385 ns). Với code nhạy độ trễ, một stall 40 µs bất ngờ giữa đường nóng là điều tệ.
Và đây là chỗ có một niềm tin ngược cũng sai — dễ mắc sau khi thấy cái giá 40 µs: "huge page nguy hiểm thế thì tắt hết cho lành". Không hẳn: tổng chi phí zero cùng một lượng bộ nhớ thì huge page lại rẻ hơn (một lần zero 2 MB hiệu quả hơn 512 lần zero 4 KB), và với vùng lớn ngẫu nhiên nó thắng thật 1,66×. Đây là đánh đổi độ trễ lấy throughput, không phải thuần lỗ. Sự thật nằm ở chỗ: một tối ưu "miễn phí" hầu như luôn có mặt trái, và phải đo cả hai chiều trước khi bật hay tắt đại trà. Huge page (và Transparent Huge Pages tự động) đáng cho vùng lớn truy cập ngẫu nhiên sống lâu; với vùng nhỏ hay nhạy độ trễ đuôi, nó có thể hại nhiều hơn lợi — nên câu trả lời không phải "luôn bật" cũng không phải "luôn tắt", mà "bật đúng chỗ".
Về đến code hằng ngày
Điều đầu tiên: bật huge page cho đúng workload — vùng lớn, truy cập ngẫu nhiên, sống lâu. Bảng băm nhiều GB, cây/đồ thị trải rộng, bộ đệm dữ liệu lớn truy cập ngẫu nhiên là ứng viên tốt (dùng madvise(MADV_HUGEPAGE) hay MAP_HUGETLB). Vùng nhỏ (vừa TLB) hay truy cập tuần tự thì đừng kỳ vọng gì — đo cho thấy gần như không đổi.
Điều thứ hai: coi chừng đột biến độ trễ và Transparent Huge Pages tự động. THP gom trang nền có thể gây khựng bất ngờ (fault 40 µs, hoặc nhân dừng để gom/nén trang). Với dịch vụ nhạy độ trễ đuôi (p99), nhiều nơi tắt THP hoặc chuyển sang madvise (chỉ bật nơi mình yêu cầu) thay vì always. Nếu bạn thấy đột biến độ trễ khó hiểu, THP là một nghi phạm — kiểm /sys/kernel/mm/transparent_hugepage/enabled.
Chỗ này cái căng thẳng "huge page thắng throughput nhưng có thể hại độ trễ" hiện rõ ở tầng vận hành, và đã thành lời khuyên chuẩn. Redis in hẳn một cảnh báo lúc khởi động nếu THP đang always, khuyên tắt vì nó gây latency spike và làm phình bộ nhớ khi fork để lưu snapshot; MongoDB và nhiều database khác cũng khuyến nghị tắt THP trong tài liệu vận hành — chính vì cái đột biến 40 µs và việc gom trang nền. Ngược lại, đúng nơi cần throughput trên vùng lớn, người ta bật huge page tường minh: Oracle/PostgreSQL cấp SGA/shared_buffers bằng HugeTLB, JVM có -XX:+UseLargePages cho heap lớn. Mẫu số chung khớp đúng phép đo: huge page là công cụ có điều kiện — bật tường minh cho vùng lớn ngẫu nhiên cần throughput, tắt THP tự động ở dịch vụ nhạy độ trễ đuôi.
Muốn tự thấy thì kiểm cấu hình THP trên máy của bạn: cat /sys/kernel/mm/transparent_hugepage/enabled — nếu thấy [always], nhân đang tự gom huge page cho mọi vùng ẩn danh lớn. Hỏi: chương trình của bạn có vùng lớn truy cập ngẫu nhiên (thứ huge page giúp) hay chủ yếu vùng nhỏ/tuần tự (thứ huge page chỉ thêm rủi ro đột biến độ trễ)? Nếu là loại sau và bạn thấy độ trễ đuôi thất thường, thử echo madvise > .../enabled (chỉ bật khi được yêu cầu) và đo lại p99. Vài phút kiểm cờ đó — cộng với việc biết huge page có điều kiện, không phải luôn thắng — cứu bạn khỏi hai sai lầm ngược nhau: bỏ lỡ một tăng tốc thật cho vùng lớn ngẫu nhiên, và gánh một đột biến độ trễ vô cớ cho vùng chẳng cần.