Ở 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. 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, 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.
Một lần tôi đo hớ: huge page có giá của nó
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. 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. 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ệ. (Đáng nói: 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 — nên đây là đánh đổi độ trễ lấy throughput, không phải thuần lỗ.)
Bài học đo lường: một tối ưu "miễn phí" hầu như luôn có mặt trái — phải đo cả hai chiều trước khi bật đại trà. Nếu tôi chỉ đo phép chase và thấy huge page nhanh 1,66 lần, tôi đã kết luận "bật huge page ở mọi nơi"; nhưng đo thêm page fault cho thấy một đột biến độ trễ 40 µs, và đo ở vùng nhỏ cho thấy phần lớn workload chẳng hưởng lợi gì. 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ễ, nó có thể hại nhiều hơn lợi — chính vì mặt trái đó mà một số cơ sở dữ liệu khuyên tắt THP.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầ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.
Hệ quả 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.
Hệ quả thứ ba là tinh thần đo lường: một tinh chỉnh "luôn thắng" hiếm khi luôn thắng — đo ở nhiều quy mô và đo cả cái giá. Con số mang theo: huge page (2MB, một mục TLB phủ 512 trang) CHỈ giúp khi TLB là nút thắt — đo chase ngẫu nhiên: vùng 0,3MB vừa TLB chỉ 1,13× (gần như không giúp), phải tới 67MB vượt phủ TLB mới thắng 1,66×; vùng nhỏ hoặc truy cập tuần tự (prefetch che miss) thì huge vô ích; và mặt trái là page fault huge nặng — cấp+zero cả 2MB mỗi fault = ~40µs (105× một fault 4KB 385ns), một đột biến độ trễ. Huge page có điều kiện, không phải nút bấm "nhanh hơn".
Thử ba mươi giây
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. Ba mươi giây 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.