为什么 base64编码的数据压缩得这么差?

我最近压缩了一些文件,我注意到 base64编码的数据似乎压缩得非常糟糕。这里有一个例子:

  • 原始文件: 429,7MiB
  • 通过 xz -9压缩:
    13,2 MiB / 429,7 MiB = 0,031 4,9 MiB/s 1:28
  • 然后通过 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然后压缩 比 < em > 压缩之后的 base64显著更差和更慢

怎么会这样?Base64是一个无损的、可逆的算法,为什么它对压缩有如此大的影响?(我也用 gzip 进行了尝试,得到了类似的结果)。

我知道这对于 基地64然后压缩文件来说没有意义,但是大多数时候人们不能控制输入文件,而且我认为由于 base64编码的文件的实际信息密度(或者不管它叫什么)几乎与非编码的版本完全相同,因此同样是可压缩的。

56370 次浏览

压缩必然是作用于多个位的操作。试图压缩一个单独的“0”和“1”没有可能的收益。即便如此,压缩通常一次只对有限的位集起作用。xz中的 LZMA 算法不会同时考虑所有的36亿位。它查看更小的字符串(< 273字节)。

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

现在相当清楚的是,这种增长必须导致一些子字符串增长超过编码器的最大子字符串大小。这导致它们不再被压缩为单个子字符串,而是实际上被压缩为两个独立的子字符串。

当你有一个 很多的压缩(97%) ,你显然有这种情况,非常长的输入子字符串压缩作为一个整体。这意味着您还将有许多子字符串是基数-64扩展超过最大长度的编码器可以处理。

大多数通用压缩算法使用 单字节粒度单字节粒度

让我们考虑以下字符串:

"XXXXYYYYXXXXYYYY"
  • 一个运行长度编码算法将显示: “就是4个‘ X’,接着是4个‘ Y’,接着是4个‘ X’,接着是4个‘ Y’”
  • Lempel-Ziv 算法会显示: 这是字符串‘ XXXXYYYY’,后面跟着同一个字符串: 所以让我们用对第一个字符串的引用来替换第二个字符串
  • 一个霍夫曼编码算法会说: “字符串中只有两个符号,所以每个符号我只能使用一个位。”

现在让我们在 Base64中对字符串进行编码:

"WFhYWFlZWVlYWFhYWVlZWQ=="

现在所有的算法都说: “这是什么乱七八糟的?”。他们不太可能很好地压缩字符串。

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

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

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

0         1         2         3         4         5         6
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

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

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

基本上,在原始数据流中显而易见的“3乘以0x58”模式在编码后的数据流中不再显而易见,因为我们已经将字节分解成6位数据包,并将它们映射到现在看起来是随机的新字节。

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

无论采用哪种压缩方法,都会对算法性能产生严重影响。这就是为什么应该总是先压缩然后再编码。

对于加密更是如此: 先压缩,再加密。

编辑-关于 LZMA 的说明

正如 MSalters 所注意到的,xz 正在使用的 LZMA 正在处理位流而不是字节流。

尽管如此,这个算法还是会受到 Base64编码的影响,这与我之前的描述基本一致:

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

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

不是64基地。它们对库的内存需求“预设7-9与预设6相似,但使用更大的字典,并有更高的压缩和解压缩内存需求。“ https://tukaani.org/xz/xz-javadoc/org/tukaani/xz/LZMA2Options.html