【问题标题】:marisa trie suffix compression?marisa trie后缀压缩?
【发布时间】:2017-12-07 06:49:47
【问题描述】:

我正在使用 this marisa trie 库的自定义 Cython 包装器作为键值多映射。

我的 trie 条目看起来像 key 0xff data1 0xff data2 以将 key 映射到元组 (data1, data2)data1 是一个可变长度的字符串,但 data2 始终是一个 4 字节的无符号整数。 0xff 是一个分隔字节。

我知道从理论的角度来看,trie 并不是最佳的数据结构,但各种实际考虑使其成为最佳选择。

在这个用例中,我有大约 10-2000 万个键,每个键平均有 10 个数据点。 data2 对于许多条目来说是多余的(在某些情况下,data2 对于给定键的所有数据点总是相同的),所以我想到了使用最频繁的 data2 条目并添加一个 ("", base_data2)数据指向每个键。

据我所知,由于 MARISA trie 没有后缀压缩,并且对于给定的键,每个 data1 都是唯一的,我假设这将节省每个使用冗余键的数据元组 4 个字节(加上添加每个键的单个 4 字节“值”)。重建特里树后,我检查了冗余数据是否不再被存储。我预计序列化和内存大小都会大幅减少,但实际上磁盘上的 trie 从 566MB 变为 557MB(并且加载的 trie 的 RAM 使用量也有类似的减少)。

由此我得出结论,没有后缀压缩我一定是错的。我现在将带有冗余data2 编号的条目存储为key 0xff data1 0xff,因此为了测试这个理论,我删除了尾随的0xff 并调整了使用trie 的代码来应对。新的 trie 从 557MB 减少到 535MB。

因此,与删除相同数量的 4 字节序列相比,删除单个冗余尾随字节的改进提高了 2 倍,因此要么后缀压缩理论大错特错,要么它以一些非常复杂的方式实现方式。

我剩下的理论是,在 trie 的较高点添加 ("", base_data2) 条目会以某种可怕的方式抛出压缩,但当我删除更多字节时,它应该只添加 4 个字节。那是从下层开始的。

我对修复并不乐观,但我非常想知道为什么我会看到这种行为!感谢您的关注。

【问题讨论】:

  • 这闻起来很像内存对齐/填充,但我在 MARISA 代码库中找不到确凿证据。 (虽然我还没有检查过 python 包装器)。

标签: python c++ trie


【解决方案1】:

我怀疑,这是由填充引起的。

lib/marisa/grimoire/vector/vector.h中,有如下函数:

void write_(Writer &writer) const {
  writer.write((UInt64)total_size());
  writer.write(const_objs_, size_);
  writer.seek((8 - (total_size() % 8)) % 8);
}

重点是:writer.seek((8 - (total_size() % 8)) % 8);。写入每个块后,写入器填充到下一个 8 字节边界。

这解释了您所看到的行为,因为通过键的初始缩短删除的部分数据已替换为填充。

当您删除额外的字节时,它会使密钥大小低于下一个边界限制,从而导致大小发生重大变化。

实际上,这意味着,由于填充代码位于库的序列化部分中,您可能会获得预期的内存节省,但这并没有转化为磁盘节省。监控程序 RAM 使用情况应确认这一点。

如果您关心磁盘大小,那么您不妨简单地压缩序列化数据,因为 MARISA 似乎没有应用任何压缩。

【讨论】:

  • 令人着迷!感谢您挖掘并找到它。您对为什么要应用该填充有任何见解吗?有点奇怪的是,我删除的分数是 4 个字节,而不是 8 个字节,因此如果键长度是均匀分布的,那么删除额外字节不会比删除任何其他字节更能降低大小。
  • 通常会添加这样的填充,以便可以将结果数据转换为所需的类型,而不是复制到堆栈变量中。玛丽莎不会那样做。此外,填充通常在需要对齐的数据之前添加,而不是在之后。对我来说它看起来有点时髦。至于你的第二个问题:因为它是对齐的最终缓冲区,所以这种行为真的很难预测。也许它每 3 个键填充 5 个字节或类似的东西。这真的很难衡量,并且可能会因数据集而异。
猜你喜欢
  • 2016-09-13
  • 1970-01-01
  • 1970-01-01
  • 2011-01-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-01
相关资源
最近更新 更多