【问题标题】:C Library for compressing sequential positive integers用于压缩顺序正整数的 C 库
【发布时间】:2009-07-04 20:20:42
【问题描述】:

我有一个非常常见的问题,即为磁盘中的字符串数组创建索引。简而言之,我需要将每个字符串的位置存储在磁盘表示中。例如,一个非常简单的解决方案是索引数组,如下所示:

uint64 idx[] = { 0, 20, 500, 1024, ..., 103434 };

表示第一个字符串在位置 0,第二个在位置 20,第三个在位置 500,第 n 个在位置 103434。

位置总是按顺序排列的非负 64 位整数。尽管数字可能会因任何差异而有所不同,但实际上我希望典型差异在 2^8 到 2^20 的范围内。我希望这个索引在内存中被映射,并且位置将被随机访问(假设均匀分布)。

我正在考虑编写自己的代码来执行某种块增量编码或更复杂的编码,但编码/解码速度和空间之间存在许多不同的权衡,我宁愿获得一个工作库作为一个起点,甚至可以满足于没有任何自定义的东西。

有什么提示吗?一个 c 库是理想的,但一个 c++ 库也可以让我运行一些初始基准测试。

如果您仍在关注,请提供更多详细信息。这将用于在库 cmph (http://cmph.sf.net) 之上构建一个类似于 cdb (http://cr.yp.to/cdb/cdbmake.html) 的库。简而言之,它适用于内存中索引较小的基于大磁盘的只读关联映射。

由于它是一个库,我无法控制输入,但我要优化的典型用例有数百万个值,平均值大小在几千字节范围内,最大值在 2^31 .

作为记录,如果我没有找到可以使用的库,我打算在 64 个整数的块中实现增量编码,初始字节指定到目前为止的块偏移量。块本身将用树索引,给我 O(log (n/64)) 访问时间。还有太多其他选择,我不想讨论它们。我真的很期待准备好使用代码,而不是关于如何实现编码的想法。我很乐意与大家分享我所做的一切。

感谢您的帮助,如果您有任何疑问,请告诉我。

【问题讨论】:

  • 访问要求是什么?随机的 ?顺序?只有先到最后?索引值大小是多少(32、48、64 位)?索引值是完全随机的(平坦分布)还是我们可以使用一些内部关系?
  • 另一个问题...看看这个例子,索引值似乎是按递增顺序排列的。 delta 值是多少?
  • 访问应该是随机的。假设均匀分布。索引条目是 64 位整数。它们的增量遵循它们指向的值大小的相同分布:几千字节。
  • 你会在磁盘上使用与内存中相同的数据结构吗?
  • 索引应该在内存中(mmap'ed),而数据(字符串)将存在于磁盘中。如果这是一个问题,请考虑所有小端。

标签: c database data-structures encoding compression


【解决方案1】:

我使用fastbit (Kesheng Wu LBL.GOV),看来您需要一些好的、快速的和 NOW 的东西,所以 fastbit 是 Oracle 的 BBC(字节对齐位图代码,berkeleydb)的一个非常有竞争力的改进。它易于设置,而且通常非常好。

但是,如果有更多时间,您可能希望查看 gray code 解决方案,它似乎最适合您的目的。

Daniel Lemire 在code.google 上发布了许多 C/++/Java 库,我已经阅读了他的一些论文,它们非常好,在 fastbit 和列重新排序的替代方法方面取得了一些进步具有置换的格雷码。

差点忘了,我也遇到过Tokyo Cabinet,虽然我觉得它不太适合我现在的项目,如果我之前知道的话,我可能会考虑更多;),它有很大的程度互操作性,

东京内阁是用C写的 语言,并作为 C 的 API 提供, Perl、Ruby、Java 和 Lua。东京 机柜可在平台上使用 具有符合 C99 的 API 和 POSIX。

正如您提到的 CDB,TC 基准​​测试有一个 TC 模式(TC 支持针对不同性能的几个操作约束),其中读取性能超过 CDB 10 倍,写入性能超过 2 倍。

关于您的增量编码要求,我对bsdiff 非常有信心,它的性能优于任何file.exe 内容修补系统,它可能还具有一些基本接口以满足您的一般需求。

Google 的新二进制压缩应用程序courgette 可能值得一试,以防您错过新闻稿,在我看到的一个测试用例中,差异比 bsdiff 小 10 倍。

【讨论】:

  • 嗨 RandomNickName42,感谢您的指点。看起来是一个很有前途的候选人。作为记录,我在这里也找到了一个类似的库:code.google.com/p/lemurbitmapindex我会尝试一下。
  • 好像有专利。 freepatentsonline.com/6831575.html。可能很重要。
  • codeforge.lbl.gov/projects/fastbit 是 fastbit、LGPL 的开发站点,我想不是 BSD 或 MS-PL 可能是一些问题,但 LGPL 中的 L 是一些 Comphert。 ;)
  • 嗨 RandomNickName42,感谢您的编辑。我正在为 TokyoCabinet 开发一个只读竞争对手。
【解决方案2】:

您有两个相互冲突的要求:

  1. 您想要压缩非常小的项目(每个 8 字节)。
  2. 您需要对每个项目进行有效的随机访问。

第二个要求很可能对每个项目施加一个固定的长度。

【讨论】:

  • 虽然我会随机访问这些数据,但不一定需要 O(1)。例如,压缩每个包含 64 个值的块中的数字并保留一棵树来查找要解压缩的块可能会给我显着的压缩和足够的访问权限。正如我所说,问题更多是关于找到一个具有现成编码算法的库,例如 delta-encoding、elias-gamma 和可能的块块。请参阅问题stackoverflow.com/questions/523733/compress-sorted-integers,尤其是 simmon 的评论以获得另一种解释。
  • 我明白了。这可能的,但考虑到小尺寸项,在内存中维护树本身的成本相对较高。
【解决方案3】:

您究竟想压缩什么?如果你考虑索引的总空间,那么节省空间真的值得吗?

如果是这样,您可以尝试将空间分成两半并将其存储到两个表中。第一个存储(上 uint、起始索引、长度、指向第二个表的指针),第二个存储(索引、下 uint)。

为了快速搜索,可以使用B+ Tree 之类的方式实现索引。

【讨论】:

    【解决方案4】:

    几年前我为全文搜索引擎做过类似的事情。在我的例子中,每个被索引的词都会生成一个记录,该记录由一个记录号(文档 ID)和一个词号(它可以很容易地存储词偏移量)组成,需要尽可能地压缩。我使用了 delta-compression 技术,该技术利用了一个事实,即在一个文档中会多次出现相同的单词,因此记录号通常根本不需要重复。并且单词偏移量增量通常适合一两个字节。这是我使用的代码。

    由于它是在 C++ 中,因此代码可能对您没有用处,但可以作为编写压缩例程的一个很好的起点。

    请原谅匈牙利符号和散布在代码中的神奇数字。就像我说的,我很多年前写过这个:-)

    IndexCompressor.h

    //
    // index compressor class
    //
    
    #pragma once
    
    #include "File.h"
    
    const int IC_BUFFER_SIZE = 8192;
    
    //
    // index compressor
    //
    class IndexCompressor
    {
    private :
       File        *m_pFile;
       WA_DWORD    m_dwRecNo;
       WA_DWORD    m_dwWordNo;
       WA_DWORD    m_dwRecordCount;
       WA_DWORD    m_dwHitCount;
    
       WA_BYTE     m_byBuffer[IC_BUFFER_SIZE];
       WA_DWORD    m_dwBytes;
    
       bool        m_bDebugDump;
    
       void FlushBuffer(void);
    
    public :
       IndexCompressor(void) { m_pFile = 0; m_bDebugDump = false; }
       ~IndexCompressor(void) {}
    
       void Attach(File& File) { m_pFile = &File; }
    
       void Begin(void);
       void Add(WA_DWORD dwRecNo, WA_DWORD dwWordNo);
       void End(void);
    
       WA_DWORD GetRecordCount(void) { return m_dwRecordCount; }
       WA_DWORD GetHitCount(void) { return m_dwHitCount; }
    
       void DebugDump(void) { m_bDebugDump = true; }
    };
    

    IndexCompressor.cpp

    //
    // index compressor class
    //
    
    #include "stdafx.h"
    #include "IndexCompressor.h"
    
    void IndexCompressor::FlushBuffer(void)
    {
       ASSERT(m_pFile != 0);
    
       if (m_dwBytes > 0)
       {
          m_pFile->Write(m_byBuffer, m_dwBytes);
          m_dwBytes = 0;
       }
    }
    
    void IndexCompressor::Begin(void)
    {
       ASSERT(m_pFile != 0);
       m_dwRecNo = m_dwWordNo = m_dwRecordCount = m_dwHitCount = 0;
       m_dwBytes = 0;
    }
    
    void IndexCompressor::Add(WA_DWORD dwRecNo, WA_DWORD dwWordNo)
    {
       ASSERT(m_pFile != 0);
       WA_BYTE buffer[16];
       int nbytes = 1;
    
       ASSERT(dwRecNo >= m_dwRecNo);
    
       if (dwRecNo != m_dwRecNo)
          m_dwWordNo = 0;
       if (m_dwRecordCount == 0 || dwRecNo != m_dwRecNo)
          ++m_dwRecordCount;
       ++m_dwHitCount;
    
       WA_DWORD dwRecNoDelta = dwRecNo - m_dwRecNo;
       WA_DWORD dwWordNoDelta = dwWordNo - m_dwWordNo;
    
       if (m_bDebugDump)
       {
          TRACE("%8X[%8X] %8X[%8X] : ", dwRecNo, dwRecNoDelta, dwWordNo, dwWordNoDelta);
       }
    
       // 1WWWWWWW
       if (dwRecNoDelta == 0 && dwWordNoDelta < 128)
       {
          buffer[0] = 0x80 | WA_BYTE(dwWordNoDelta);
       }
       // 01WWWWWW WWWWWWWW
       else if (dwRecNoDelta == 0 && dwWordNoDelta < 16384)
       {
          buffer[0] = 0x40 | WA_BYTE(dwWordNoDelta >> 8);
          buffer[1] = WA_BYTE(dwWordNoDelta & 0x00ff);
          nbytes += sizeof(WA_BYTE);
       }
       // 001RRRRR WWWWWWWW WWWWWWWW
       else if (dwRecNoDelta < 32 && dwWordNoDelta < 65536)
       {
          buffer[0] = 0x20 | WA_BYTE(dwRecNoDelta);
          WA_WORD *p = (WA_WORD *) (buffer+1);
          *p = WA_WORD(dwWordNoDelta);
          nbytes += sizeof(WA_WORD);
       }
       else
       {
          // 0001rrww
          buffer[0] = 0x10;
    
          // encode recno
          if (dwRecNoDelta < 256)
          {
             buffer[nbytes] = WA_BYTE(dwRecNoDelta);
             nbytes += sizeof(WA_BYTE);
          }
          else if (dwRecNoDelta < 65536)
          {
             buffer[0] |= 0x04;
             WA_WORD *p = (WA_WORD *) (buffer+nbytes);
             *p = WA_WORD(dwRecNoDelta);
             nbytes += sizeof(WA_WORD);
          }
          else
          {
             buffer[0] |= 0x08;
             WA_DWORD *p = (WA_DWORD *) (buffer+nbytes);
             *p = dwRecNoDelta;
             nbytes += sizeof(WA_DWORD);
          }
    
          // encode wordno
          if (dwWordNoDelta < 256)
          {
             buffer[nbytes] = WA_BYTE(dwWordNoDelta);
             nbytes += sizeof(WA_BYTE);
          }
          else if (dwWordNoDelta < 65536)
          {
             buffer[0] |= 0x01;
             WA_WORD *p = (WA_WORD *) (buffer+nbytes);
             *p = WA_WORD(dwWordNoDelta);
             nbytes += sizeof(WA_WORD);
          }
          else
          {
             buffer[0] |= 0x02;
             WA_DWORD *p = (WA_DWORD *) (buffer+nbytes);
             *p = dwWordNoDelta;
             nbytes += sizeof(WA_DWORD);
          }
       }
    
       // update current setting
       m_dwRecNo = dwRecNo;
       m_dwWordNo = dwWordNo;
    
       // add compressed data to buffer
       ASSERT(buffer[0] != 0);
       ASSERT(nbytes > 0 && nbytes < 10);
       if (m_dwBytes + nbytes > IC_BUFFER_SIZE)
          FlushBuffer();
       CopyMemory(m_byBuffer + m_dwBytes, buffer, nbytes);
       m_dwBytes += nbytes;
    
       if (m_bDebugDump)
       {
          for (int i = 0; i < nbytes; ++i)
             TRACE("%02X ", buffer[i]);
          TRACE("\n");
       }
    }
    
    void IndexCompressor::End(void)
    {
       FlushBuffer();
       m_pFile->Write(WA_BYTE(0));
    }
    

    【讨论】:

      【解决方案5】:

      您省略了有关要索引的字符串数量的关键信息。

      但鉴于您说您希望索引字符串的最小长度为 256,将索引存储为 64% 会产生 最多 3% 的开销。如果字符串文件的总长度小于 4GB,则可以使用 32 位索引并产生 1.5% 的开销。这些数字向我表明,如果压缩很重要,最好压缩字符串,而不是索引。对于这个问题,LZ77 的变体似乎是有序的。

      如果您想尝试一个疯狂的想法,请将每个字符串放在一个单独的文件中,将它们全部拉入一个 zip 文件,然后看看如何使用 zziplib。这可能不会很好,但您的工作几乎为零。

      欢迎提供有关该问题的更多数据:

      • 字符串数
      • 字符串的平均长度
      • 字符串的最大长度
      • 字符串的中值长度
      • 使用gzip 压缩字符串文件的程度
      • 是否允许您更改字符串的顺序以提高压缩率

      编辑

      评论和修改后的问题使问题更加清晰。我喜欢你的分组想法,我会尝试简单的增量编码,对增量进行分组,并在每个组中使用可变长度代码。我不会将 64 作为组大小进行连接——我认为您可能希望根据经验确定这一点。

      您要求提供现有库。对于分组和增量编码,我怀疑你会发现很多。对于可变长度整数代码,我看不到 C 库的方式,但您可以在 PerlPython 中找到可变长度编码。关于这个主题有大量的论文和一些专利,我怀疑你最终将不得不自己动手。但是有一些简单的代码,你可以试试 UTF-8——它可以编码高达 32 位的无符号整数,你可以从 Plan 9 获取 C 代码,我相信还有很多其他来源。

      【讨论】:

      • 嗨 Norman,字符串的数量大约为数亿,平均长度为 10k。最大长度为 2^31。完整的字符串集(值)不适合内存,并且无法重新排序。在实际方面,我正在使用它来构建一个库,所以我并不能真正控制输入。这些数字代表我过去看到的用例(网页)。
      • 喂!好的,您的编辑使问题更加清晰。如果您找到现成的解决方案,我会印象深刻。我在答案中添加了几个链接。
      【解决方案6】:

      您在 Windows 上运行吗?如果是这样,我建议使用您最初提出的简单解决方案创建 mmap 文件,然后使用 NTLM 压缩 压缩文件。您的应用程序代码永远不会知道文件已压缩,而操作系统会为您进行文件压缩。您可能认为这不会有很好的性能或压缩效果,但我认为如果您尝试一下,您会感到惊讶。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-06-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-08-07
        • 2012-09-21
        • 1970-01-01
        相关资源
        最近更新 更多