关于压缩:为什么base64编码的数据压缩如此差?

Why does base64-encoded data compress so poorly?

我最近正在压缩一些文件,并且我发现以base64编码的数据似乎压缩得非常糟糕。这是一个示例:

  • 原始文件:429,7 MiB
  • 通过xz -9压缩:
    13,2 MiB / 429,7 MiB = 0,031 4,9 MiB/s 1:28
  • base64它并通过xz -9进行压缩:
    26,7 MiB / 580,4 MiB = 0,046 2,6 MiB/s 3:47
  • base64原始压缩的xz文件:
    17,8 MiB几乎没有时间=预期的1.33x大小增加

所以可以观察到的是:

  • xz压缩真的好吗?
  • base64编码的数据无法很好地压缩,比未编码的压缩文件大2倍
  • base64-then-compress比compress-then-base64差得多,而且慢

怎么可能? Base64是一种无损,可逆的算法,为什么它会如此严重地影响压缩? (我也尝试使用gzip,结果相似)。

我知道先base64-然后压缩文件是没有意义的,但是大多数情况下,人们无法控制输入文件,而且我会认为,由于实际的信息密度(或以其他方式调用)以base64编码的文件将与未编码的版本几乎相同,因此可以类似地压缩。


大多数通用压缩算法以一字节的粒度工作。

让我们考虑以下字符串:

1
"XXXXYYYYXXXXYYYY"
  • 运行长度编码算法将说:"那是4 \\'X \\',后跟4 \\'Y \\',然后是4 \\'X \\',再是4 \\'Y \\'"
  • Lempel-Ziv算法会说:"那是字符串\\'XXXXYYYY \\',后跟相同的字符串:所以让我们将第二个字符串替换为对第一个的引用。"
  • 霍夫曼编码算法会说:"该字符串中只有2个符号,所以每个符号只能使用一位。"

现在让我们在Base64中编码我们的字符串。这是我们得到的:

1
"WFhYWFlZWVlYWFhYWVlZWQ=="

所有算法现在都在说:"那是什么烂摊子?"。而且它们不太可能很好地压缩该字符串。

提醒一下,Base64基本上是通过将(0 ... 255)中的3个字节的组重新编码为(0 ... 63)中的4个字节的组来工作的:

1
2
Input bytes    : aaaaaaaa bbbbbbbb cccccccc
6-bit repacking: 00aaaaaa 00aabbbb 00bbbbcc 00cccccc

每个输出字节然后转换为可打印的ASCII字符。按照惯例,这些字符是(这里每10个字符带有一个标记):

1
2
0         1         2         3         4         5         6
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

例如,我们的示例字符串以三个字节的组开头,十六进制(字符" X "的ASCII代码)等于0x58。或以二进制格式:01011000。让我们应用Base64编码:

1
2
3
4
5
6
Input bytes      : 0x58     0x58     0x58
As binary        : 01011000 01011000 01011000
6-bit repacking  : 00010110 00000101 00100001 00011000
As decimal       : 22       5        33       24
Base64 characters: 'W'      'F'      'h'      'Y'
Output bytes     : 0x57     0x46     0x68     0x59

基本上,在原始数据流中显而易见的模式"字节0x58的3倍"在编码数据流中不再明显,因为我们已将字节分成6位数据包并将其映射到现在看来是随机的新字节。

或者换句话说:我们已经打破了大多数压缩算法所依赖的原始字节对齐方式。

无论使用哪种压缩方法,通常都会严重影响算法性能。这就是为什么您应该始终先压缩然后再编码的原因。

对于加密更是如此:首先压缩,然后加密。

编辑-关于LZMA的注释

正如MSalters所注意到的那样,xz正在使用的LZMA在位流而不是字节流上工作。

不过,该算法也将在某种程度上与我先前的描述相一致的方式遭受Base64编码的困扰:

1
2
3
4
5
Input bytes      : 0x58     0x58     0x58
As binary        : 01011000 01011000 01011000
(see above for the details of Base64 encoding)
Output bytes     : 0x57     0x46     0x68     0x59
As binary        : 01010111 01000110 01101000 01011001

即使在位级别上工作,也比在输出二进制序列中识别输入二进制序列中的模式要容易得多。


压缩必定是作用于多个位的操作。尝试压缩单个" 0 "和" 1 "没有任何好处。即使这样,压缩通常一次只能作用于一组有限的位。 xz中的LZMA算法不会一次考虑所有36亿位。它查找的字符串要小得多(<273字节)。

现在看一下base-64编码的作用:它使用256个可能的值中的仅64个将3字节(24位)字替换为4字节字。这使您获得x1.33的增长。

现在,很明显,这种增长必须导致某些子字符串超过编码器的最大子字符串大小。这导致它们不再被压缩为单个子字符串,而是确实被压缩为两个单独的子字符串。

由于压缩量很大(97%),因此很显然,很长的输入子字符串会整体压缩。这意味着您还将用base-64扩展许多子字符串,使其超过编码器可以处理的最大长度。


它不是Base64。 "预设7-9与预设6相似,但是使用更大的字典,并且具有更高的压缩器和解压缩器存储需求。" https://tukaani.org/xz/xz-javadoc/org/tukaani /xz/LZMA2Options.html