Đây là nút chỉnh mà mọi lập trình viên đều gặp và ít ai đo: mức nén (compression level). Bạn gõ gzip -9 vì nghĩ "số to = tốt nhất", hoặc để zstd -19 cho "nén tối đa". Nhưng con số đó không miễn phí — và thường bạn đang trả một cái giá khổng lồ về thời gian để đổi lấy một chút xíu dung lượng. Sự thật là: quan hệ giữa mức nén và tỉ lệ nén không tuyến tính — nó tuân theo luật lợi ích giảm dần (diminishing returns), và điểm cân bằng hầu như không bao giờ là mức cao nhất. Bài này (phần 6 loạt Nén) đo thật gzip và zstd trên cùng một khối dữ liệu 11MB để bạn thấy đường cong đó bằng số, và biết chọn mức nào.

Level đổi công sức, không đổi thuật toán

Điều quan trọng đầu tiên: gzip -1 tới -9 (và zstd -1 tới -19) vẫn chạy cùng một thuật toán (DEFLATE / zstd). Cái thay đổi là bao nhiêu công sức bộ tìm khớp bỏ ra:

  • Level thấp: tìm khớp nhanh — chỉ cần khớp "đủ tốt", không cố tìm khớp dài nhất, cửa sổ tìm hẹp hơn. Nhanh, nhưng bỏ lỡ nhiều cơ hội nén.
  • Level cao: tìm khớp kỹ lưỡng — cố tìm khớp dài nhất, thử nhiều vị trí hơn, đôi khi "nhìn xa" hơn. Nén tốt hơn, nhưng chậm hơn nhiều.

Và một điểm ít người biết: giải nén thì giống nhau ở mọi level. Level chỉ ảnh hưởng lúc nén; file nén ở -1 hay -9 đều giải nén nhanh như nhau. Điều này rất quan trọng cho quyết định "nén một lần, đọc nhiều lần" ở cuối bài.

Ảnh chụp đoạn mã nền tối minh hoạ mức nén đánh đổi tốc độ lấy tỉ lệ luật lợi ích giảm dần gzip -1 tới -9 zstd -1 tới -19 điểm cân bằng ở đâu, level không đổi thuật toán đổi công sức tìm khớp gzip -1 tới -9 và zstd -1 tới -19 vẫn là DEFLATE zstd chỉ khác bộ tìm khớp chịu bỏ bao nhiêu công sức level thấp tìm khớp nhanh đủ tốt nhanh nén ít level cao tìm khớp dài nhất kỹ lưỡng chậm nén nhiều hơn giải nén thì giống nhau mọi level level chỉ ảnh hưởng lúc nén, đo cùng dữ liệu ở mọi level size cộng time for lv trong range 1 10 t comp bằng best_time lambda zlib compress data lv đo cả kích thước len comp và thời gian perf_counter lấy min vẽ đường cong size giảm dần time tăng dần theo level, điều cần tìm lợi ích giảm dần diminishing returns câu hỏi thực tế tăng level có đáng không thường L1 tới L6 size giảm nhiều time tăng vừa đáng L6 tới L9 size giảm rất ít time tăng mạnh thường không đáng đo thật trên dữ liệu của bạn rồi chọn đừng mặc định chọn -9, khi nào -9 -19 mới đáng nén một lần đọc tải nhiều lần asset tĩnh bản phát hành backup lâu dài chi phí nén cao trả 1 lần tiết kiệm băng thông đĩa mỗi lần đọc đáng nén theo luồng realtime log response HTTP động tốc độ quan trọng level thấp

Hình 1: Level không đổi thuật toán mà đổi công sức tìm khớp (thấp = nhanh/nén ít, cao = chậm/nén nhiều); giải nén giống nhau mọi level; đo cả size lẫn time từng level để thấy lợi ích giảm dần, rồi chọn theo cách dùng thay vì mặc định -9.

Đo thật: đường cong lợi ích giảm dần

Mình tạo 11MB log truy cập mô phỏng (có cấu trúc, biến thiên vừa phải như log thật), nén ở mọi level và đo cả kích thước lẫn thời gian:

Ảnh chụp bảng kết quả chạy thật mức nén output thật go-lab 11MB log mô phỏng python zlib cộng zstd CLI, gzip zlib level 1 tới 9 trên 10957188 byte level size B ratio time ms vs L1 level 1 2065392 5.3x 27.2 1.0x level 3 1797433 6.1x 37.9 1.4x level 6 1502875 7.3x 82.9 3.0x mặc định cân bằng level 7 1441019 7.6x 104.9 3.9x level 8 1398938 7.8x 242.6 8.9x level 9 1395025 7.9x 288.8 10.6x nhỏ hơn L6 khoảng 7 phần trăm chậm 3.5x, zstd -1 tới -19 cùng quy luật còn kịch tính hơn zstd size ratio time -1 1757728 6.2x 19.8 nhanh đã tốt -3 1712890 6.4x 33.8 -9 1342983 8.2x 91.6 điểm hợp lý -15 1179239 9.3x 695.1 -19 1115709 9.8x 3302.9 từ -9 tới -19 nhỏ hơn khoảng 17 phần trăm chậm 36x, kết chọn level theo cách dùng đừng mặc định -9

Hình 2: Chạy thật trên 11MB — gzip level 1-9: L1 2065KB/27ms, L6 1503KB/83ms (7.3x), L9 1395KB/289ms (7.9x); zstd -1 1758KB/20ms, -9 1343KB/92ms (8.2x), -19 1116KB/3303ms (9.8x); mức cao đổi thời gian gấp hàng chục lần lấy vài % dung lượng.

  • gzip: từ -6 lên -9 gần như vô nghĩa: level 1 cho 2065KB trong 27ms. Lên level 6 (mặc định), xuống 1503KB trong 83ms — nén tốt hơn nhiều (5.3x → 7.3x), thời gian tăng vừa phải (3x). Nhưng từ level 6 lên level 9: kích thước chỉ giảm từ 1503KB xuống 1395KB — nhỏ hơn ~7% — trong khi thời gian nhảy từ 83ms lên 289ms (chậm ~3.5 lần). Bạn trả gấp 3.5 lần thời gian cho 7% dung lượng. Với hầu hết trường hợp, đó là món hời tồi.
  • zstd: còn kịch tính hơn ở cuối thang: zstd -1 đã cho 6.2x trong 20ms (nhanh hơn cả gzip -9!). -9 cho 8.2x trong 92ms — điểm hợp lý. Nhưng lên -19: tỉ lệ tăng từ 8.2x lên 9.8x (nhỏ hơn ~17%) trong khi thời gian nhảy từ 92ms lên 3303ms — chậm gấp 36 lần. Cùng một luật lợi ích giảm dần, chỉ là đuôi dốc hơn hẳn.
  • Điểm cân bằng gần đầu thang, không phải cuối: cả hai đường cong đều cho thấy phần lớn giá trị nằm ở những mức đầu (1→6 với gzip, 1→9 với zstd), còn những mức cuối chỉ vắt kiệt vài phần trăm cuối bằng cái giá thời gian tăng vọt. Mặc định gzip là -6 và zstd là -3 chính vì lý do này — chúng được chọn làm điểm cân bằng, không phải điểm nén tối đa.

Đánh đổi cần cân nhắc

"Nén một lần, đọc nhiều lần" là khi mức cao mới đáng. Vì giải nén nhanh như nhau ở mọi level, nếu bạn nén một asset tĩnh (file JS/CSS phát hành cho hàng triệu lượt tải), một bản phát hành, hay một backup lưu trữ lâu dài — chi phí nén cao trả một lần, còn dung lượng nhỏ hơn tiết kiệm băng thông/đĩa mỗi lần đọc. Ở đây -9/-19 (thậm chí các công cụ chuyên như zopfli, brotli -11) đáng đồng tiền. Đây là ngoại lệ hợp lý của quy tắc "đừng dùng mức cao nhất".

Nén theo luồng, realtime thì tốc độ quan trọng hơn dung lượng. Ngược lại, khi nén log đang chảy, response HTTP động, dữ liệu qua đường ống thời gian thực — mỗi mili-giây nén làm tăng độ trễ. Ở đây mức thấp (gzip -1, zstd -1) hoặc thuật toán nhanh (LZ4, phần 7) là lựa chọn đúng: nén "đủ" để giảm băng thông mà không thành nút cổ chai. Nhiều CDN và proxy nén HTTP động ở mức thấp chính vì điều này.

Đo trên dữ liệu của bạn — đường cong khác nhau theo dữ liệu. Các con số trong bài là cho log mô phỏng; dữ liệu của bạn (JSON, ảnh, nhị phân, text tiếng Việt...) có đường cong khác. Đừng chép con số của người khác — chạy thử vài level trên mẫu thật, vẽ size vs time, rồi chọn. Việc này mất vài phút và cứu bạn khỏi vừa chậm vừa chẳng nén hơn bao nhiêu (như bài profiling ở loạt Debug: đo trước khi tối ưu).

Ba ý mang về

  1. Level tuân theo luật lợi ích giảm dần: đo thật gzip từ -6 lên -9 chỉ nhỏ hơn ~7% nhưng chậm ~3.5 lần; zstd từ -9 lên -19 nhỏ hơn ~17% nhưng chậm ~36 lần — phần lớn giá trị nằm ở các mức đầu, không phải mức cuối.
  2. Mặc định (-6 gzip, -3 zstd) là điểm cân bằng có chủ đích: level đổi công sức tìm khớp chứ không đổi thuật toán, và giải nén nhanh như nhau ở mọi level; đừng mặc định gõ -9/-19 nghĩ "tốt nhất".
  3. Chọn mức theo cách dùng, đo trên dữ liệu của bạn: nén một lần đọc nhiều lần (asset tĩnh, backup) thì mức cao đáng; nén realtime/luồng thì mức thấp; và luôn đo size vs time trên mẫu thật vì đường cong phụ thuộc dữ liệu.

Nguồn

Phần sau ta không chỉ chỉnh level mà đổi hẳn thuật toán: gzip vs bzip2 vs xz vs zstd — đo thật tỉ lệ nén và tốc độ của cả bốn trên cùng dữ liệu để biết khi nào chọn cái nào.