Hãy hình dung một người đóng gói hành lý cực khéo. Cho họ một vali quần áo gấp lộn xộn, họ xếp lại gọn ghẽ, dồn được vào nửa vali. Nhưng nếu bạn đưa họ một vali đã xếp tối ưu từ trước — hay tệ hơn, một vali toàn viên bi rời chẳng theo trật tự nào — thì tài khéo của họ vô dụng: không còn khoảng trống nào để dồn. Nén dữ liệu đúng là người đóng gói đó. Nó không tạo ra chỗ trống bằng phép màu; nó chỉ khai thác sự lộn xộn có quy luật. Và khi dữ liệu không còn quy luật, nó bó tay — có khi còn làm gói to thêm.

Ai cũng dùng né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ế hay bị bỏ qua: dữ liệu nào nén được, và nén bao nhiêu; rồi "nén mạnh hơn" có luôn đáng không. Tôi đo cả hai bằng zlib, bz2, lzma trên vài megabyte mỗi loại, và cả hai lần trực giác của tôi đều trật.

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

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

Nhưng nén vấp một giới hạn cứng mà không mẹo nào vượt: entropy — lượng thông tin thật sự chứa 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; càng ngẫu nhiên thì càng ít. Một dãy thật sự ngẫu nhiên thì không nén được lấy một byte — nó chẳng có dư thừa nào để moi. Đây là câu "không có bữa trưa miễn phí" của nén, và tôi đo nó thẳng tay.

Cùng công cụ, tỉ lệ 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ì bàn làm gì):

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ệ 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, thay hết bằng con trỏ, cắt gần sạch. 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 thì đây là chỗ tôi ngã ngửa lần đầu: 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 buộc phải thêm, trong khi chẳng cắt được gì. Nén không nặn ra 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.

Mức 9 không đáng như tôi tưởng

Định kiến thứ hai của tôi: "cần nén tốt thì cứ để 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 việc tìm kiếm sâu hơn nhưng chẳng moi ra được gì để cắt thêm. 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ệ chừ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ó chỉ 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 hơn: lzma chậm 23 lần zlib. Và một điều đáng nhớ nữa: "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 ngược. Không có "thuật toán nén tốt nhất" chung chung.

Về đến code hằng ngày

Điều đầ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 ở trên. Nhiều hệ thống dính 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 một 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ó.

Điều 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, 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. Vẫn là 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ỏ.

Và chuyện này bám sát code thật ở mọi tầng. HTTP: một bộ Spring Boot bật server.compression hay một tầng nginx gzip sẽ nén JSON/HTML response rất đáng, nhưng nếu bạn cấu hình nó nén cả ảnh đã tối ưu thì lại trả CPU cho con số âm — nhớ giới hạn mime-types cho đúng loại còn dư thừa. Java Deflater cho bạn chỉnh setLevel(1..9) đúng đường cong lợi giảm dần ở trên; Go có gzip.NewWriterLevel và cộng đồng ngày càng ngả về zstd vì nó cho tỉ lệ gần lzma ở tốc độ gần lz4. Cơ sở dữ liệu cũng chung một câu chuyện: Postgres TOAST tự nén cột lớn, cột đã lưu ảnh/blob nén sẵn thì bật nén thêm chỉ phí; nhiều cột kho dữ liệu cho chọn zstd vs lz4 đúng theo cán cân "đọc nhiều hay ghi nhiều". Mẫu số chung: 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" luôn là một đánh đổi phải đo trên dữ liệu thật của bạn.

Muốn tự thấy thì trên máy bạn thử gzip -k vài file khác loại rồi so kích thước: một file .txt hay .json (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 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 giờ (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 đó gói trọn cả hai bài học — giới hạn entropy và lợi giảm dần — mà chẳng cần một dòng lý thuyết.