为什么 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-then-compress一个文件没有意义,但大多数时候人们无法控制输入文件,我可能会认为,由于 base64 编码文件的实际信息密度(或任何名称)与非编码版本几乎相同,因此可以类似地压缩。
压缩必然是作用于多个位的操作。尝试压缩单个“0”和“1”不可能有任何好处。即便如此,压缩通常一次只对有限的一组位进行压缩。 xz
中的 LZMA 算法不会一次考虑所有 36 亿位。它查看更小的字符串(<273 字节)。
现在看看 base-64 编码的作用:它将 3 字节(24 位)字替换为 4 字节字,仅使用 256 个可能值中的 64 个。这为您提供了 x1.33 的增长。
现在很明显,这种增长必然会导致一些子字符串增长超过编码器的最大子字符串大小。这导致它们不再被压缩为单个子字符串,而是实际上是两个单独的子字符串。
由于您有 很多 的压缩 (97%),您显然会遇到很长的输入子字符串被整体压缩的情况。这意味着您还将有许多子字符串被 base-64 扩展超过编码器可以处理的最大长度。
大多数通用压缩算法使用 一个字节的粒度。
让我们考虑以下字符串:
"XXXXYYYYXXXXYYYY"
- A 运行-长度编码算法会说:"that's 4 'X', followed by 4 'Y', followed by 4 'X', followed by 4 'Y'"
- Lempel-Ziv 算法会说:"That's the string 'XXXXYYYY', followed by the same string: so let's replace the 2nd string with a reference to the 1st one."
- 哈夫曼编码算法会说:"There are only 2 symbols in that string, so I can use just one bit per symbol."
现在让我们用 Base64 编码我们的字符串。这是我们得到的:
"WFhYWFlZWVlYWFhYWVlZWQ=="
所有算法现在都在说:"What kind of mess is that?"。而且他们不太可能很好地压缩该字符串。
提醒一下,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(字符的 ASCII 代码 "X")。或者二进制: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
即使在位级别上工作,识别输入二进制序列中的模式也比识别输出二进制序列中的模式容易得多。
这不是 Base64。它对库的内存要求 "The presets 7-9 are like the preset 6 but use bigger dictionaries and have higher compressor and decompressor memory requirements."https://tukaani.org/xz/xz-javadoc/org/tukaani/xz/LZMA2Options.html
我最近在压缩一些文件,我注意到 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-then-compress一个文件没有意义,但大多数时候人们无法控制输入文件,我可能会认为,由于 base64 编码文件的实际信息密度(或任何名称)与非编码版本几乎相同,因此可以类似地压缩。
压缩必然是作用于多个位的操作。尝试压缩单个“0”和“1”不可能有任何好处。即便如此,压缩通常一次只对有限的一组位进行压缩。 xz
中的 LZMA 算法不会一次考虑所有 36 亿位。它查看更小的字符串(<273 字节)。
现在看看 base-64 编码的作用:它将 3 字节(24 位)字替换为 4 字节字,仅使用 256 个可能值中的 64 个。这为您提供了 x1.33 的增长。
现在很明显,这种增长必然会导致一些子字符串增长超过编码器的最大子字符串大小。这导致它们不再被压缩为单个子字符串,而是实际上是两个单独的子字符串。
由于您有 很多 的压缩 (97%),您显然会遇到很长的输入子字符串被整体压缩的情况。这意味着您还将有许多子字符串被 base-64 扩展超过编码器可以处理的最大长度。
大多数通用压缩算法使用 一个字节的粒度。
让我们考虑以下字符串:
"XXXXYYYYXXXXYYYY"
- A 运行-长度编码算法会说:"that's 4 'X', followed by 4 'Y', followed by 4 'X', followed by 4 'Y'"
- Lempel-Ziv 算法会说:"That's the string 'XXXXYYYY', followed by the same string: so let's replace the 2nd string with a reference to the 1st one."
- 哈夫曼编码算法会说:"There are only 2 symbols in that string, so I can use just one bit per symbol."
现在让我们用 Base64 编码我们的字符串。这是我们得到的:
"WFhYWFlZWVlYWFhYWVlZWQ=="
所有算法现在都在说:"What kind of mess is that?"。而且他们不太可能很好地压缩该字符串。
提醒一下,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(字符的 ASCII 代码 "X")。或者二进制: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
即使在位级别上工作,识别输入二进制序列中的模式也比识别输出二进制序列中的模式容易得多。
这不是 Base64。它对库的内存要求 "The presets 7-9 are like the preset 6 but use bigger dictionaries and have higher compressor and decompressor memory requirements."https://tukaani.org/xz/xz-javadoc/org/tukaani/xz/LZMA2Options.html