如何高效地预测数据是否可压缩

23
我想编写一个存储后端来存储大块数据。数据可以是任何东西,但主要是二进制文件(图像、pdf、jar 文件)或文本文件(xml、jsp、js、html、java 等)。我发现大多数数据已经被压缩了。如果一切都被压缩,可以节省约 15% 的磁盘空间。

我正在寻找最有效的算法,可以高概率预测一个数据块(假设为 128 KB)是否可压缩(无损压缩),而不必查看所有数据。

压缩算法将是 LZF、Deflate 或类似的算法(可能是 Google Snappy)。因此,预测数据是否可压缩应该比压缩数据本身快得多,并且使用的内存更少。

我已经知道的算法:

  • 尝试压缩数据子集,例如 128 字节(这有点慢)
  • 计算 128 字节的总和,如果在某个范围内,则很可能不能压缩(在 128 * 127 的 10% 范围内)(这很快,相对较好,但我正在寻找更可靠的算法,因为该算法实际上只查看每个字节的最高位)
  • 查看文件头(相对可靠,但感觉像作弊)

我想大致的思路是需要一个算法,可以快速计算字节列表中每个位的概率是否大约为 0.5。

更新

我实现了“ASCII 检查”、“熵计算”和“简化压缩”,所有方法都给出了良好的结果。我想完善算法,现在我的想法不仅是预测数据是否可以压缩,而且还要预测它可以被压缩的程度。可能使用多种算法的组合。如果只能接受多个答案……我会接受给出最佳结果的答案。

如果有额外的答案(新想法),请继续提供!如果可能的话,请附带源代码或链接。:-)

更新2

现在在Linux中实现了一种类似的方法,详情请见


你可以尝试一种统计方法(显然你已经考虑过了),或者根据文件类型事先进行一些估计。我会选择第二个选项并在此基础上进行改进。 - James P.
在纯统计学中有多种可能的方法。其中一个显而易见的方法是选择随机元素。 - James P.
1
我发现只需查看前128字节就足以很好地预测一个块是否可压缩。但这只是一个例子。 - Thomas Mueller
1
如果你主要处理完整的文件,那么我建议使用文件头,甚至只用前4个字节来识别已知的压缩文件格式。Linux 的 file 命令使用了一个非常广泛的魔法模式数据库,你可以从中提取所需的信息。 - Jörn Horstmann
1
@Thomas 不确定前128个字节。有些格式在这里有可压缩的头文件。例如,JAR/zip文件有文件名列表,大多数情况下像纯文本,但后面有压缩内容。也许值得对整个块采样几个小块。 - kan
显示剩余5条评论
9个回答

8

我实现了一些方法来测试数据是否可压缩。

简化压缩

这主要是检查重复的字节对:

static boolean isCompressible(byte[] data, int len) {
    int result = 0;
    // check in blocks of 256 bytes, 
    // and sum up how compressible each block is
    for (int start = 0; start < len; start += 256) {
        result += matches(data, start, Math.min(start + 255, len));
    }
    // the result is proportional to the number of 
    // bytes that can be saved
    // if we can save many bytes, then it is compressible
    return ((len - result) * 777) < len * 100;
}

static int matches(byte[] data, int i, int end) {
    // bitArray is a bloom filter of seen byte pairs
    // match counts duplicate byte pairs
    // last is the last seen byte
    int bitArray = 0, match = 0, last = 0;
    if (i < 0 || end > data.length) {
        // this check may allow the JVM to avoid
        // array bound checks in the following loop
        throw new ArrayIndexOutOfBoundsException();
    }
    for (; i < end; i++) {
        int x = data[i];
        // the bloom filter bit to set
        int bit = 1 << ((last ^ x) & 31);
        // if it was already set, increment match
        // (without using a branch, as branches are slow)
        match -= (-(bitArray & bit)) >> 31;
        bitArray |= bit;
        last = x;
    }
    return match;
}

在我的(有限的)测试数据集上,这个算法非常准确。如果数据无法压缩,则它比自身压缩快约5倍。但对于琐碎的数据(全为零),速度大约慢一半。
部分熵:
该算法估计高四位中的熵值。我想避免使用过多的桶,因为每次都必须清空它们(如果要检查的块很小,则这样做会很慢)。 63 - numberOfLeadingZeros 是对数(我想避免使用浮点数)。根据数据不同,它的速度可能比上面的算法快或慢(不确定原因)。结果没有上面的算法那么精确,可能是因为只使用了16个桶,且只进行整数运算。
static boolean isCompressible(byte[] data, int len) {
    // the number of bytes with 
    // high nibble 0, 1,.., 15
    int[] sum = new int[16];
    for (int i = 0; i < len; i++) {
        int x = (data[i] & 255) >> 4;
        sum[x]++;
    }
    // see wikipedia to understand this formula :-)
    int r = 0;
    for (int x : sum) {
        long v = ((long) x << 32) / len;
        r += 63 - Long.numberOfLeadingZeros(v + 1);
    }
    return len * r < 438 * len;
}

哇,我不怕位操作,但这需要一些注释或命名常量,特别是“2777”部分。另外,last*2777会不会溢出? - Jörn Horstmann
这只是初始版本... 最终版本将有注释。该代码类似于LZF和其他压缩算法。last*2777应该会溢出,因为它是一个哈希函数。 - Thomas Mueller
感谢您的解释,使用一组字节的哈希函数并计算重复值是一个不错的想法。 - Jörn Horstmann
我猜现在你已经有这些方法的最终版本了,能否更新一下答案? - Rui Marques
我实际上还没有使用(在[MVStore]中的代码(http://h2database.com/html/mvstore.html)),但我已经试图注释了代码。 - Thomas Mueller

8

计算数据的熵值。如果它的熵值高(约为1.0),则不太可能继续压缩。如果它的熵值低(约为0.0),那么这意味着其中没有很多“信息”并且可以进一步压缩。

它提供了一个理论上的测量,评估一个数据可以被压缩到什么程度。


2
熵只是一些简单压缩技术(例如使用普通的哈夫曼编码)的好度量标准。常用的压缩格式(gzip、bzip、lzma)使用更复杂的算法,因此仅靠熵无法确定数据是否可以被压缩。 - jarnbjo
3
“@jarnbjo:这是衡量最佳压缩技术能达到的程度。我不明白为什么这还不够。无论算法多么复杂,它都无法超越数据的熵。” - tskuzzy
@tskuzzy:当然,更确切地说,这取决于您(压缩器)对熵的确切理解或者您如何定义熵计算的输入字母表。简单的字节计数是不够的。 - jarnbjo
1
@tskuzzy:请告诉我如何执行这个神奇的通用熵计算。例如,根据您对熵的理解,字节序列0..255重复1000次的熵是多少? - jarnbjo
2
jarnbjo是正确的。您似乎暗示计算数据的(真实)熵是很简单的,但事实并非如此;您需要做出一些假设,例如字节是独立的。但是一个文件可能所有字节的概率相等,但仍然具有高冗余(低熵)。而且,像gzip这样的压缩器利用了这种冗余,这很难作为概率模型来衡量。 - leonbloy
显示剩余6条评论

8

根据我的经验,几乎所有可以有效压缩的格式都是非二进制的。因此,检查大约70-80%的字符是否在[0-127]范围内应该就可以解决问题。

如果你想要“正确”地做(尽管我真的看不出有理由这样做),你要么必须对数据运行(部分)压缩算法,要么计算熵,就像tskuzzy已经建议的那样。


这听起来就像我之前想的一样。计算128字节的总和是检查大多数字符是否在[0-127]范围内的简单方法,这个我已经有了。使用一个简化的压缩算法(没有输出)也是一个好主意,可能比计算熵更好(这是我之前没有考虑过的)。 - Thomas Mueller
我已经实现了“ASCII检查”、“熵计算”和“简化压缩”(请参见下面的答案),并且所有结果都很好。我想要完善这些算法,现在我的想法不仅是预测数据是否可以被压缩,而且还可以预测它可以被压缩多少。可能需要使用多种算法的组合。如果我能够接受多个答案就好了……我将接受给出最佳结果的答案。 - Thomas Mueller
我将使用部分压缩算法(见下面的答案)。我发现熵计算也不错,但并不完全如此,并且有时会慢一些。 - Thomas Mueller

4

这个问题本身很有趣,因为例如使用zlib对无法压缩的数据进行压缩比压缩可压缩数据要花费更长时间。因此,进行不成功的压缩尤其昂贵(详见链接)。IBM的Harnik等人在这个领域做了很好的最近工作。

是的,前缀方法和0字节顺序熵(在其他帖子中称为熵)是很好的指标。

还有其他判断文件是否可以压缩的好方法(来自论文):

  • 核心集大小 - 组成大多数数据的字符集
  • 符号对分布指示器

这是FAST的论文幻灯片


2

估算可压缩性的更快、更准确的算法

  1. 比使用Shannon熵判断2到4倍更快、更准确。它基于Huffman编码方法。
  2. 回答的时间复杂度不取决于符号频率的数值,而是取决于唯一符号的数量。Shannon熵计算log(频率),因此频率越高,计算这个值所需的时间也越长。在当前的方法中,避免了对频率值进行数学运算。
  3. 出于与上述原因相同的原因,精度也更高,因为我们避免了对浮点数的依赖,仅依赖于求和和乘法运算以及实际的Huffman编码如何对总压缩大小产生贡献。
  4. 相同的算法可以增强以在较短的时间内生成实际的Huffman编码,这不涉及像树、堆或优先队列这样的复杂数据结构。对于我们的不同要求,我们只使用相同的符号频率数组。

以下算法指定了如何计算存储在映射数组中的文件符号频率值的可压缩性。时间比较图表

     int compressed_file_size_in_bits = 0, n=256;
  /* We sort the map array in increasing order.
   * We will be simulating huffman codes algorithm.
   * Insertion Sort is used as its a small array of 256 symbols.
   */
  insertionSort(map, 256);

  for (j = 0; j < n; j++)
    if (map[j] != 0)
      break;

  for (i = j; i + 1 < n; i++) {
    j = i + 1;
    /* Following is an important step, as we keep on building more
     * and more codes bottom up, their contribution to compressed size
     * gets governed by following formula. Copy pen simulation is recommended.
     */
    compressed_file_size_in_bits = compressed_file_size_in_bits + map[i] + map[j];

    /* Least two elements of the map gets summed up and form a new frequency
     * value which gets placed at i+1 th index.
     */
    map[i + 1] = map[i] + map[j];
    // map [i+2-----] is already sorted. Just fix the first element.
    Adjust_first_element(map + i + 1, n - i - 1);
  }
  printf("Entropy per byte %f ", compressed_file_size_in_bits * (1.0) / file_len);

  void insertionSort(long arr[], long n) {
  long i, key, j;
  for (i = 1; i < n; i++) {
    key = arr[i];
    j = i - 1;

    /* Move elements of arr[0..i-1], that are
    greater than key, to one position ahead
    of their current position */
    while (j >= 0 && arr[j] > key) {
      arr[j + 1] = arr[j];
      j = j - 1;
    }
    arr[j + 1] = key;
  }
}

// Assumes arr[i+1---] is already sorted. Just first
// element needs to be placed at appropriate place.
void Adjust_first_element(long arr[], long n) {
  long i, key, j = 1;
  key = arr[0];
  while (j < n && arr[j] < key) {
    arr[j - 1] = arr[j];
    j = j + 1;
  }
  arr[j - 1] = key;
}

使用上述算法构建代码

使用上述算法构建代码是一个字符串操作问题,我们从每个符号都没有代码开始。然后,我们遵循与压缩文件大小/可压缩性计算相同的算法。此外,我们只需不断维护代码演变的历史记录。在迭代频率数组完成后,我们最终的代码包含每个符号的不同Huffman代码的演变,并存储在代码数组的顶部索引中。 此时,一个字符串解析算法可以解析这种演变,并为每个符号生成单独的代码。整个过程不涉及树、堆或优先队列。只需要一次迭代通过频率数组(在大多数情况下为大小256)即可生成代码的演变以及最终压缩大小值。

   /* Generate code for map array of frequencies. Final code gets generated at
 * codes[r] which can be provided as input to string parsing algorithm to
 * generate code for individual symbols.
 */
void generate_code(long map[], int l, int r) {
  int i, j, k, compressed_file_size_in_bits = 0;

  insertionSort(map + l, r - l);

  for (i = l; i + 1 <= r; i++) {
    j = i + 1;

    compressed_file_size = compressed_file_size_in_bits + map[i] + map[j];
    char code[50] = "(";

    /* According to  algorithm, two different codes from two different
     * nodes are getting combined in a way so that they can be separated by
     * by a string parsing algorithm.  Left node code, codes[i] gets appended by
     * 0  and right node code, codes[j] gets appended by 1. These two codes 
      get
     * separated by a comma.
     */
    strcat(code, codes[i]);
    strcat(code, "0");
    strcat(code, ",");
    strcat(code, codes[j]);
    strcat(code, "1");
    strcat(code, ")");

    map[i + 1] = map[i] + map[j];

    strcpy(codes[i + 1], code);

    int n = r - l;
    /* Adjust_first_element now takes an additional 3rd argument.
     * this argument helps in adjusting codes according to how
     * map elements are getting adjusted.
     */
    Adjust_first_element(map + i + 1, n - i - 1, i + 1);
  }
}

void insertionSort(long arr[], long n) {
  long i, key, j;
  //   if(n>3)
  //  n=3;
  for (i = 1; i < n; i++) {
    key = arr[i];
    j = i - 1;

    /* Move elements of arr[0..i-1], that are
    greater than key, to one position ahead
    of their current position */
    while (j >= 0 && arr[j] > key) {
      arr[j + 1] = arr[j];
      j = j - 1;
    }
    arr[j + 1] = key;
  }
}

// Assumes arr[i+1---] is already sorted. Just first
// element needs to be placed at appropriate place.
void Adjust_first_element(long arr[], long n, int start) {
  long i, key, j = 1;
  char temp_arr[250];
  key = arr[0];
  /* How map elements will change position, codes[] element will follow
   * same path
   */
  strcpy(temp_arr, codes[start]);

  while (j < n && arr[j] < key) {
    arr[j - 1] = arr[j];
    /* codes should also move according to map values */
    strcpy(codes[j - 1 + start], codes[j + start]);
    j = j + 1;
  }
  arr[j - 1] = key;
  strcpy(codes[j - 1 + start], temp_arr);
}

1

我觉得在尝试压缩之前,没有办法检查某个东西的可压缩性。 你可以检查模式(更多模式可能意味着更可压缩),但是特定的压缩算法可能不使用你检查的模式 - 并且可能比你预期的效果更好。 另一个技巧可能是取前128000字节的数据,通过Deflate/Java压缩,然后看是否小于原始大小。如果是这样 - 很有可能整体压缩是值得的。


1

像LZ4这样的快速压缩器已经内置了数据可压缩性的检查。它们会快速跳过不好的段,集中精力处理更有趣的部分。 为了提供一个恰当的例子,LZ4在非可压缩数据上几乎以RAM速度极限运行(在我的笔记本电脑上为2GB/s)。因此,探测器要更快几乎没有余地。 你可以自己尝试一下: http://code.google.com/p/lz4/


我不同意,探测器可以更快。LZ4与LZO、LZF和Snappy类似,我已经知道它们有多快。所有这些压缩算法都会检测无法压缩的块,但是它们检测相对较慢。 - Thomas Mueller
“Slowly”听起来有点过分了。最好的压缩算法(我不包括LZF在内)已经可以在RAM速度限制下处理不可压缩数据,并且在提供适当输出(大多数情况下是输入的副本,如果它是不可压缩的)的同时完成任务。只需删除输出以提供压缩计数器统计信息,这可能是最快的方法。 - Cyan
正如我所写的,我对Java算法很感兴趣。请注意,我写的是“相对缓慢”,而不仅仅是“缓慢”:根据我的测试,最快压缩算法的Java版本比我的算法慢得多(我的算法不生成输出,也不使用真正的哈希表)。 - Thomas Mueller
Lz4 很快,但是在相同的无法压缩数据上,Snappy通常能够胜过Lz4。 - u0b34a0f6ae

0

你的个人资料上写着你是H2数据库引擎的作者,这是一个用Java编写的数据库。

如果我猜得没错的话,你想要设计这个数据库引擎来自动压缩BLOB数据,如果可能的话。

但是——(我猜)你已经意识到并不是所有的东西都可以压缩,速度很重要——所以你不想浪费一微秒来确定是否应该压缩数据......

我的问题是工程性质的——为什么要这样做?基本上,这不是在以速度为代价来猜测数据库用户/应用程序开发人员的意图吗?

你不认为一个应用程序开发人员(首先将数据写入blob字段的人)会是决定数据是否应该被压缩,以及选择适当的压缩方法的最佳人选吗?

我唯一能看到自动数据库压缩可能增加一些价值的地方是文本/varchar字段——只有当它们超过一定长度时——但即使如此,这个选项也可能更好地由应用程序开发人员决定......我甚至可以允许应用程序开发人员使用压缩插件,如果需要的话......这样他们就可以为自己的数据做出自己的决策......

如果我对你所尝试做的事情的假设是错误的,那么我谦卑地为我所说的话道歉...(这只是一个微不足道的用户意见。)


1
这个功能实际上是我正在为Jackrabbit 3考虑的,而不是为H2考虑。这并不会影响速度(这是计划中的)。进行一些简单的内存计算比将数据存储到磁盘中要快。如果数据可以被大量压缩,那么压缩+存储压缩文件可能比仅存储未压缩文件更快。 - Thomas Mueller
为什么不在后台压缩数据呢?如果有可用的CPU周期,您可以创建一个线程来查找可压缩的对象,尝试对它们进行压缩,如果成功,则将它们作为压缩后的文件写回,如果失败,则跳过它们并继续执行。线程检查CPU状态,如果CPU正在忙碌,则进入休眠状态...? - Peter Sherman
在后台进行压缩是一个好主意,但这更加复杂,特别是因为数据存储应该是不可变的。用压缩版本替换数据很棘手,因为数据存储可以同时从另一个进程中访问。此外,如果需要存储两次数据,则总吞吐量将降低。 - Thomas Mueller

0

另外-为什么不试一试lzop呢?我个人保证它比bzip、gzip、zip、rar...压缩和解压速度更快。

http://www.lzop.org

将其用于磁盘映像压缩会使过程受到磁盘IO的限制。使用任何其他压缩器都会使过程受到CPU的限制(即,其他压缩器使用所有可用的CPU,而lzop(在合理的CPU上)可以以与7200 RPM存储硬盘相同的速度处理数据...)

我敢打赌,如果你用“测试压缩”字符串的前X个字节进行测试,它比大多数其他方法快得多...


你知道LZF和Snappy吗?它们与LZO属于同一类别。你能否指向一个开源的、纯Java实现的LZO压缩算法(目前可用的不是开源的)? - Thomas Mueller
我找到了一个版本的LZO,但它是用C语言编写的,并使用了特殊的预处理器/转换器转换成Java。我还找到了一些基准测试结果,显示纯Java的LZF(源代码可用)和Snappy(本地!)与LZO速度大致相同。 - Thomas Mueller

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