Nén dữ liệu nghe như một phép màu: cùng một nội dung, ít byte hơn, miễn phí. Ai cũng dùng nó — file .zip, ảnh .png, request HTTP có gzip. Nhưng phép màu ấy có luật, và hai câu hỏi thực tế thường bị bỏ qua: dữ liệu nào nén được (và nén bao nhiêu), và "nén mạnh hơn" có luôn đáng không? Bài này đo cả hai bằng zlib, bz2, lzma — và tìm ra hai chỗ mà trực giác sai.

Nén dữ liệu

Nén khai thác dư thừa, và entropy đặt trần

Nén hoạt động bằng cách tìm và loại bỏ dư thừa (redundancy). Hai họ chính. LZ (Lempel-Ziv, nền của zip/gzip): khi gặp một đoạn đã xuất hiện trước đó, thay nó bằng một con trỏ ngắn "quay lại N byte, chép M byte" thay vì lặp lại. Huffman/mã entropy: ký tự hay gặp được gán mã bit ngắn, ký tự hiếm mã dài — như chọn cấu trúc theo tần suất. zlib/gzip ghép cả hai.

Nhưng nén có một giới hạn cứng, không mẹo nào vượt được: entropy — lượng thông tin thật sự trong dữ liệu. Dữ liệu càng có quy luật (lặp, đoán được, phân bố lệch) thì càng nhiều dư thừa để cắt; dữ liệu càng ngẫu nhiên thì càng ít. Và một dãy thật sự ngẫu nhiên thì không nén được chút nào — nó không có dư thừa nào để khai thác. Đây là "không có bữa trưa miễn phí" của nén, và tôi đo nó trực tiếp.

Đo: cùng công cụ, tỉ lệ nén nhảy từ 514 lần xuống dưới 1

Tôi nén 3 loại dữ liệu bằng cùng zlib mức 6, mỗi loại vài megabyte, và kiểm giải nén ra đúng gốc (nén có mất thì vô nghĩa):

Tỉ lệ nén (gốc / nén) theo loại dữ liệu, cùng zlib mức 6:
  lặp lại nhiều ("ca phe va code "...) : 514,24x
  văn bản (từ vựng nhỏ)                :   2,66x
  byte NGẪU NHIÊN                      :   1,00x   -> 8.000.000 → 8.000.316 byte, PHÌNH THÊM!

Cùng một thuật toán, tỉ lệ nén nhảy từ 514 lần (dữ liệu lặp) xuống 2,66 lần (văn bản) xuống dưới 1 (ngẫu nhiên). Với dữ liệu lặp, LZ tìm thấy vô số đoạn trùng và thay bằng con trỏ, cắt gần hết. Với văn bản, còn dư thừa (từ lặp, chữ cái phân bố lệch) nên nén được vài lần. Với byte ngẫu nhiên, kết quả là chỗ đo hớ đầu tiên của tôi: nó không nhỏ đi mà lớn ra — 316 byte phình thêm là phần khung (header, bảng) mà nén phải thêm vào, trong khi chẳng cắt được gì. Nén không tạo trật tự từ hỗn loạn; entropy đặt một cái trần, và với dữ liệu đã ngẫu nhiên (hay đã nén rồi — như JPEG, MP4, file .zip), cái trần đó nằm trên kích thước gốc.

Đo hớ thứ hai: mức 9 không đáng như tôi tưởng

Định kiến thứ hai: "cần nén tốt thì để mức cao nhất (9), thiệt gì đâu". Tôi đo tỉ lệ và thời gian của zlib ở các mức trên cùng 8 MB văn bản:

zlib trên 8 MB văn bản:
  mức 1: tỉ lệ 2,42x |  46 ms
  mức 3: tỉ lệ 2,44x |  55 ms
  mức 6: tỉ lệ 2,66x |  93 ms
  mức 9: tỉ lệ 2,66x |  91 ms   (bằng hệt mức 6)

Mức 9 cho tỉ lệ y hệt mức 6 (2,66x) trên dữ liệu này — công sức thêm đổ vào tìm kiếm sâu hơn nhưng không tìm ra gì để cắt thêm. Và nhìn cả đường cong: mức 1 đã đạt 2,42x chỉ trong nửa thời gian mức 6; đi từ mức 1 lên mức 9 chỉ cải thiện tỉ lệ khoảng 10% (2,42 → 2,66) mà tốn gấp đôi thời gian. Đây là lợi giảm dần kinh điển — cùng dáng với điểm giao và đánh đổi mà sê-ri đã gặp nhiều lần. "Mức cao nhất" không phải mặc định đúng; nó là một điểm trên đường cong đánh đổi, và với nhiều dữ liệu, mức thấp cho gần hết lợi ích ở phần nhỏ chi phí.

Còn giữa các thuật toán thì sao? Tôi so zlib, bz2, lzma trên cùng văn bản:

Cùng 8 MB văn bản, tỉ lệ nén | thời gian nén:
  zlib (mức 6) : 2,66x |    89 ms
  bz2  (mức 9) : 4,08x |   300 ms   (chặt hơn, chậm 3,4 lần)
  lzma         : 3,13x | 2.076 ms   (chậm 23 lần zlib)

Nén chặt hơn luôn tốn thời gian nhiều hơn: lzma chậm 23 lần zlib. Và một điều đáng nhớ khác: "thuật toán nào nén tốt nhất" phụ thuộc dữ liệu — ở đây bz2 (dùng biến đổi BWT) lại cho tỉ lệ tốt nhất (4,08x) trên văn bản từ-vựng-nhỏ này, còn lzma vừa chậm hơn vừa nén kém hơn bz2. Trên dữ liệu khác thứ tự có thể đảo. Không có "thuật toán nén tốt nhất" chung.

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

Hệ quả đầu tiên: đừng nén cái đã ngẫu nhiên hoặc đã nén. Gzip một file JPEG, MP4, hay .zip chỉ tốn CPU mà làm file to hơn (thêm khung, không cắt được gì) — đúng cái phình 316 byte trong bài. Nhiều hệ thống mắc lỗi này: bật nén tầng vận chuyển cho nội dung đã nén, trả tiền CPU cho con số âm. Trước khi nén, hỏi: dữ liệu này còn dư thừa không? Ảnh/video/archive thì không; văn bản, JSON, log, mã nguồn thì có.

Hệ quả thứ hai: chọn mức và thuật toán theo đánh đổi của bạn, đừng mặc định max. Nén để gửi qua mạng thời gian thực (một API response) cần nhanh — mức thấp của zlib, hay các thuật toán hiện đại như zstd/lz4 ưu tiên tốc độ. Nén để lưu trữ lâu dài, ghi một lần đọc nhiều (một bản backup, một release artifact) thì đáng dùng mức cao hay lzma — trả thời gian nén một lần để tiết kiệm dung lượng mãi mãi. Cùng bài toán "trả trước hay trả sau" của khấu hao: tần suất dùng quyết định mức đầu tư đáng bỏ ra.

Hệ quả thứ ba là bài học đo lường bao trùm: tỉ lệ nén là thuộc tính của dữ liệu, không phải của công cụ, và "nén mạnh hơn" là một đánh đổi phải đo. Con số mang theo: cùng zlib, tỉ lệ nén nhảy từ 514x (dữ liệu lặp) xuống 2,66x (văn bản) xuống dưới 1 với byte ngẫu nhiên (phình +316 byte, entropy đặt trần); và zlib mức 9 cho tỉ lệ y hệt mức 6 trong khi lzma chặt hơn nhưng chậm 23 lần — chọn mức/thuật toán theo dữ liệu và ràng buộc, đừng mặc định max. Trước khi bật nén, đo tỉ lệ trên dữ liệu thật của bạn, và cân tốc độ với tỉ lệ theo đúng cách bạn sẽ dùng nó.

Thử ba mươi giây

Trên máy bạn, thử gzip -k một vài file khác loại và so kích thước: một file .txt hay .json (sẽ nhỏ đi vài lần), rồi một file .jpg hay .mp4 (gần như không đổi, có khi to hơn). Bạn sẽ thấy tận mắt entropy đặt trần: cái đã nén không nén thêm được. Rồi thử gzip -1 so gzip -9 trên cùng file văn bản lớn và đo thời gian (time gzip -1 file so time gzip -9 file): mức 9 chậm hơn hẳn mà file ra thường chỉ nhỏ hơn vài phần trăm. Hai thí nghiệm ba mươi giây đó cho bạn cả hai bài học của bài — giới hạn entropy và lợi giảm dần — mà không cần một dòng lý thuyết nào.