为什么Base64编码的数据压缩效果很差?

82
最近我在压缩一些文件时,发现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 对原始压缩的xz文件进行base64编码:
几乎没有时间的17.8 MiB=预期的1.33x增加大小
因此可以观察到: xz压缩非常好☺ base64编码数据不易压缩,比未编码的压缩文件大两倍 先进行base64编码-然后压缩比先压缩-然后base64编码要差得多且慢
这怎么可能呢? Base64 是一种无损可逆算法,为什么会影响压缩效果这么大呢?(我也试过gzip,结果类似)
我知道对一个文件进行base64编码然后压缩是没有意义的,但大多数情况下,人们无法控制输入文件,我认为由于经过base64编码的文件的实际信息密度(或者叫做其他什么)几乎与未编码的版本相同,因此应该可以进行类似地压缩。

2
“_base64-then-compress_”的效率和速度明显比“_compress-then-base64_”差很多,更不用说这两者在实质上毫无关联了。 - MSalters
3个回答

187

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

让我们来考虑下面的字符串:

"XXXXYYYYXXXXYYYY"
  • 一个Run-Length-Encoding算法会说:"这是4个'X',后面是4个'Y',再接着是4个'X',最后是4个'Y'"
  • 一个Lempel-Ziv算法会说:"这是字符串 'XXXXYYYY',后面跟着相同的字符串:所以我们用对第一个字符串的引用来代替第二个字符串。"
  • 一个Huffman编码算法会说:"那个字符串中只有2个符号,因此我可以每个符号使用一个比特位。"

现在让我们使用Base64编码我们的字符串。这是我们得到的结果:

"WFhYWFlZWVlYWFhYWVlZWQ=="

现在所有的算法都在说:“这是什么混乱?”。他们不可能将那个字符串压缩得很好。

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

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所注意到的,LZMA(即xz正在使用的算法)是针对位流而非字节流进行操作的。

然而,这个算法也会受到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
即使在位级别上工作,识别输入二进制序列中的模式比识别输出二进制序列中的模式要容易得多。

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


1
感谢详细的解释。因此,base64正在“混淆”位流中的重复部分。有趣的是,@MSalters指出问题的一部分是我的数据可以很好地压缩,如果我提供来自/dev/urandomxz数据,则不会压缩(如预期),但是通过base64/dev/urandom压缩到约77%,非常接近通过霍夫曼编码的理论最大值75%。 - Stefan Seidel
2
是的,实现Huffman编码的任何算法都至少会发现在Base64编码的字符串中只使用了256个符号(在这种情况下是字节)中的64个。对于随机数据,所有64个符号应获得均匀分布,因此导致压缩比约为75%。 - Arnauld
25
加1分解释:所有算法现在都在说:“这是什么混乱?” - user305883
3
如果一个压缩工具足够聪明,能够检测到Base64编码,然后改变其压缩算法以更好地处理它(例如,在压缩之前先解码),那将是很酷的。 - Jordan Rieger

11

压缩必然是对多个比特位进行操作。试图压缩单个的“0”和“1”是没有可能获得任何优势的。尽管如此,压缩通常只对一组有限的比特位进行操作,如在xz中使用的LZMA算法,它不会一次考虑所有的36亿比特位,而是只查看更小的字符串(<273字节)。

现在看看base-64编码的作用:用64种可能值中的一种,将3个字节(24个比特位)替换为4个字节,这样就得到了1.33倍的增长。

显然,这种增长必然会导致某些子字符串超过编码器允许的最大子字符串大小。这将导致它们不再被压缩为单个子字符串,而是被拆分成两个独立的子字符串来压缩。

由于有很多压缩(97%),很明显你所面临的情况是非常长的输入子字符串被整体压缩。这意味着你也会有许多子字符串在被解码时超过编码器能处理的最大长度。


-8

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接