【发布时间】: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<string,int> 用于将字符串块映射到索引到List<string>,并且可以在构建 trie 后丢弃,尽管 GC 似乎没有删除它(在此之前完成了几个显式集合dump) - SOS 中的 !gcroot 不表示任何根,但我预计以后的 GC 会释放它。
MiniList<T> 是 List<T> 的替代品,使用精确大小(即线性增长,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