【问题标题】:Space-efficient in-memory structure for sorted text supporting prefix searches用于支持前缀搜索的排序文本的节省空间的内存结构
【发布时间】:2010-11-24 04:52:42
【问题描述】:

我有一个问题:我需要基于文件路径前缀的文件系统数据的节省空间的查找。换句话说,排序文本的前缀搜索。你说用特里,我也这么想。麻烦的是,尝试不够节省空间,并非没有其他技巧。

我有大量数据:

  • 磁盘上的纯文本 Unix 格式列表中大约 450M
  • 大约 800 万行
  • gzip 默认压缩到 31M
  • bzip2 默认压缩到 21M

我不想在内存中接近 450M 的地方吃东西。在这一点上,我很乐意使用大约 100M 的速度,因为前缀形式存在大量冗余。

我正在使用 C# 来完成这项工作,而直接实现 trie 仍然需要文件中的每一行都有一个叶节点。假设每个叶节点都需要对最终文本块的某种引用(32 位,例如字符串数据数组的索引以最小化字符串重复),并且 CLR 对象开销为 8 个字节(使用 windbg / SOS 验证) , 我将在结构开销上花费 >96,000,000 字节,根本没有文本存储。

让我们看一下数据的一些统计属性。塞进树丛中时:

  • 大约 110 万个独特的文本“块”
  • 文本文件中磁盘上大约 16M 的唯一块总数
  • 平均块长度为 5.5 个字符,最大 136 个
  • 当不考虑重复时,块中总共有大约 5200 万个字符
  • 内部 trie 节点平均约 6.5 个子节点,最多 44 个子节点
  • 大约 180 万个内部节点。

叶子创建的超额率约为 15%,内部节点创建的超额率为 22% - 超额创建是指在 trie 构建期间创建的叶子和内部节点,但不是在最终 trie 中创建的最终节点数的比例每种类型。

这是来自 SOS 的堆分析,表明使用最多的内存:

 [MT    ]--[Count]----[   Size]-[Class                                          ]
 03563150       11         1584 System.Collections.Hashtable+bucket[]
 03561630       24         4636 System.Char[]
 03563470        8         6000 System.Byte[]
 00193558      425        74788      Free
 00984ac8    14457       462624 MiniList`1+<GetEnumerator>d__0[[StringTrie+Node]]
 03562b9c        6     11573372 System.Int32[]
*009835a0  1456066     23297056 StringTrie+InteriorNode
 035576dc        1     46292000 Dictionary`2+Entry[[String],[Int32]][]
*035341d0  1456085     69730164 System.Object[]
*03560a00  1747257     80435032 System.String
*00983a54  8052746     96632952 StringTrie+LeafNode

Dictionary&lt;string,int&gt; 用于将字符串块映射到索引到List&lt;string&gt;,并且可以在构建 trie 后丢弃,尽管 GC 似乎没有删除它(在此之前完成了几个显式集合dump) - SOS 中的 !gcroot 不表示任何根,但我预计以后的 GC 会释放它。

MiniList&lt;T&gt;List&lt;T&gt; 的替代品,使用精确大小(即线性增长,O(n^2) 附加性能)T[] 以避免空间浪费;这是一个值类型,InteriorNode 使用它来跟踪孩子。这个T[] 被添加到System.Object[] 堆中。

所以,如果我把“有趣”的项目(标有*)加起来,我得到大约 270M,这比磁盘上的原始文本要好,但仍然不够接近我的目标。我认为 .NET 对象开销太大,并创建了一个新的“slim”trie,仅使用值类型数组来存储数据:

class SlimTrie
{
    byte[] _stringData; // UTF8-encoded, 7-bit-encoded-length prefixed string data

    // indexed by _interiorChildIndex[n].._interiorChildIndex[n]+_interiorChildCount[n]
    // Indexes interior_node_index if negative (bitwise complement),
    // leaf_node_group if positive.
    int[] _interiorChildren;

    // The interior_node_index group - all arrays use same index.
    byte[] _interiorChildCount;
    int[] _interiorChildIndex; // indexes _interiorChildren
    int[] _interiorChunk; // indexes _stringData

    // The leaf_node_index group.
    int[] _leafNodes; // indexes _stringData

    // ...
}

这个结构已经把数据量减少到了139M,对于只读操作来说仍然是一个高效的可遍历trie。而且因为它非常简单,我可以轻松地将它保存到磁盘并恢复它,以避免每次都重新创建 trie 的成本。

那么,对于前缀搜索比 trie 更有效的结构,有什么建议吗?我应该考虑的替代方法?

【问题讨论】:

  • 您打算如何使用这些数据?大量处理或只是几个查找;您能否就有效存储和处理之间的权衡取舍给出一些想法?
  • 它基本上是缓存文件系统的查找操作,这样就不需要查询物理磁盘,例如获取目录中的所有文件、递归目录中的所有文件等,无需咨询磁盘,它总是不在内存中,实际上是通过网络=>太多往返。性能预期是执行 150 次前缀查找(即查找具有此前缀的所有行)返回平均 100 行所花费的时间不应超过 100 毫秒。实际上,我的 SlimTrie 方法需要 10 秒才能从磁盘加载并列出 8,000,000 行 => ~18ms。
  • 关闭优化,开启优化,8.5 秒 - 包括应用启动。 140M还不错,但考虑到这个数据的冗余,我相信它可以改进。

标签: c# .net algorithm prefix trie


【解决方案1】:

现成的想法:用哈希表代替 trie。您的内存中只有散列和字符串数据,可能是压缩的。

或者你能负担得起一页的阅读费用吗?只有哈希和文件在内存中的位置,检索“页面”与该哈希匹配的行,大概是少量的有序行,因此在发生冲突时可以快速搜索。

【讨论】:

  • Doing 150 试图从每个位置读取 100 行并不像人们希望的那样快——这就是我在采用 trie 方法之前的做法。我在文本文件中使用了行索引,即一个文件基本上包含到每行开头的 32 位偏移的平面数组,文件按排序顺序排列。随机搜索超过 450M 的文件杀死你。
  • 对于哈希表的想法 - 我不太了解你。前缀搜索不是固定长度的键,它可能是 a/b、a/b/c、a/b/c/d 等。使用索引存储一次字符数据。
  • 这个想法是散列整个前缀,不管多长时间。这将产生一个数字,该数字是“页面”的索引,该页面包含与该哈希匹配的所有行。因此,您只进行一次逻辑读取,取回一些行。 [这实际上可能是一些物理读取,但希望少于 150 次搜索。] 然后您只需丢弃任何您不想要的哈希冲突。
  • 问题是我会尝试在不到 100 毫秒的时间内进行 150 次单独的前缀查找,所以即使我有一个哈希将前缀映射到文件中的确切位置,它可能仍然太慢了。
【解决方案2】:

由于只有 110 万个块,您可以使用 24 位而不是 32 位来索引一个块并节省空间。

您也可以压缩块。或许Huffman coding 是个不错的选择。我还会尝试以下策略:您应该对字符转换进行编码,而不是使用字符作为符号进行编码。因此,与其查看字符出现的概率,不如查看状态为当前字符的Markov chain 中的转换概率。

【讨论】:

  • Huffman 树是我在看到特里树中的块后写的第一件事 - 我正在考虑尝试将行编码为位字符串,每个块一个字符串,连接起来 - 但当我在编写位打包逻辑时,我考虑过使用平面值类型数组来代替 trie 编码。正确有效地实现霍夫曼编码,尤其是解码,很快就会变得相当乏味。我可能会将其取回,并可能根据字符频率进行编码。
  • 是的,我考虑过使用少于 32 位的索引。其他事情:16M 字符数据接近 24 位,但如果我将字符数据与字边界对齐,每个块平均花费 0.5 字节,我可以使用 24 位来索引高达 32M 的位置,节省一半。我为 Huffman 树编码编写的位打包逻辑可能对使用少于整数个字节来存储索引很有用。我的下一步可能是编写一个“位域数组”类。
  • 我将授予这个胜利。我编写了一个位压缩数组类,它可以索引恒定位宽的有符号或无符号整数,并确定从可变加载时间 StringTrie 转换为不可变 SlimTrie 时所需的最大宽度。将 SlimTrie 存储在磁盘上并稍后重新加载可以节省时间和内存,避免过时的 GC 垃圾。现在降至 75M!
  • 我第一次错过了用于字符对编码的马尔可夫链,有趣的想法可能/可能会大大减少文本存储,因为即使在前缀和重复块之后也存在一定程度的自相似性已经得到照顾。
【解决方案3】:

您可以找到一篇与您的问题相关的科学论文here(作者引用:“实验表明,我们的索引支持在接近通过 gzip 压缩字符串字典可实现的空间占用的空间占用内进行快速查询, bzip 或 ppmdi。” - 但不幸的是,该文件仅用于付款)。我不确定这些想法实施起来有多么困难。本文的作者有一个website,您还可以在其中找到各种压缩索引算法的实现(在“索引集合”下)。

如果您想继续您的方法,请务必查看有关 Crit-bit treesRadix tree 的网站。

【讨论】:

  • 清理了他要求的任何喜欢的人的答案(他只能链接到一个网站)
  • 没关系,我订阅了 ACM。我会调查一下。
  • 实际上,基数树或 Patricia trie 是我存储 trie 数据的方式 - 每个边/节点仅存储一个字符显然不适合空间定位。
  • 除了霍夫曼编码,您还可以考虑范围编码 (en.wikipedia.org/wiki/Range_encoding)。
猜你喜欢
  • 2016-02-14
  • 2010-09-26
  • 1970-01-01
  • 1970-01-01
  • 2016-10-28
  • 2021-08-01
  • 1970-01-01
  • 2013-08-18
  • 1970-01-01
相关资源
最近更新 更多