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 "没有任何好处。即使这样,压缩通常一次只能作用于一组有限的位。
现在看一下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