【问题标题】:C++ efficient and compact map with integer keys具有整数键的 C++ 高效且紧凑的映射
【发布时间】:2016-07-30 16:33:05
【问题描述】:

我有 200 组大约 50,000 个唯一整数,范围在 0 到 500,000 之间,我需要映射到另一个小值(整数对,值不相关,因此没有按需计算)。

我尝试使用 std::unordered_maps,这使用了大约 50MB(在 VS2015 堆诊断工具中测量),虽然性能很好,但我想降低这个内存使用量(打算在一些小的 500MB 上作为后台服务云服务器)。

实际上我的初始版本是 200 个独立的 std::unordered_map<int, std::pair<int, int>>

一个选项似乎是排序数组并使用二分查找,但还有其他选择吗?

【问题讨论】:

  • 200 个“集合”中的每一个都有自己独特的地图吗?
  • 你试过std::map吗?
  • @Galik 既没有空间效率,也没有像 std::unordered_map 在这种情况下那样高效。我更好奇是否对存储桶大小进行了任何调整。
  • 200 * 50000 * 4 字节的整数等于 40 兆字节。所以在 50 兆字节(包括映射值)的情况下,我会说你做得很好。
  • 假设集合是动态的(因为它们必须是有序数组才能正常工作),我会考虑我在旧答案中概述的技术:stackoverflow.com/a/9754470/179910

标签: c++ optimization data-structures


【解决方案1】:

如果您在排序后不更改向量,我认为排序向量应该可以工作。它真的很节省空间,即没有指针开销。

如果您需要更好的性能,并且不介意一些第三方库。你可以试试sparse_hash_map,它用很少的空间开销实现了哈希映射。

【讨论】:

    【解决方案2】:

    我想最高效的内存将是std::vector<std::pair<int, std::set<Something>>>,就像你已经建议的那样。

    在这种情况下,您只会因为以下原因而产生内存开销:

    • std::vector 的固定开销(非常有限)
    • 有时在“增长”期间内存使用量会更高,因为此时旧数据和新数据必须处于活动状态
    • std::vector 中未使用的空间

    您有点表示在构建之后您不再需要扩展向量,因此您可以reserveshrink_to_fit 摆脱未使用的空间。 (请注意,reserve 还修复了增长期间内存使用量的峰值)

    如果您需要更密集的使用,您可以考虑将存储更改为std::vector<std::set<Something>>std::vector<std::unique_ptr<std::set<Something>>>。在这种结构中,索引是隐式的,尽管内存增益只会显示您是否为每个索引都有一个值。

    使用向量的缺点是您必须编写一些自定义代码。在这种情况下,std::unordered_mapstd::map 并不是那么糟糕,如果您不介意处理器缓存(L1 ...)上更多的缓存未命中以实现较少标准的实现,可以查看Googles sparsehash、@987654322 @ 或 Facebooks AtomicHashMap from Folly,虽然我没有任何经验。

    最后,人们可能想知道为什么您将这些数据都保存在内存中,尽管如果您需要最佳性能,我看不出有什么方法可以防止这种情况发生。

    【讨论】:

    • 我不明白set::set 的工作原理。 Something 是什么样的?至于带有排序数组的自定义代码,计划只使用std::sort(创建后)和std::lower_bound(查找)。
    • 除非你的意思是Something 是值并且数组索引是键?就像我说的数据是从 0 到 500,000 的 50,000 个数字,所以使用这样的数组只有大约 10% 的效率。在 64 位平台上 sizeof(unique_ptr) 也将与 2 个整数一样大,尽管我认为我可以为它们设置一个“无效值”(也许是 INT_MAX)。
    • 确实,它代表了一些存储空间,因为我不确定您的代表。 (或本帖的下一位读者)
    • 确实,64 位使指针或空集的技巧更加无用(如果您不打算使用 int64_t)。虽然如果您最初的情况需要这样做,它可能会很方便。
    • 另请注意,集合也可以通过向量进行更改。
    【解决方案3】:

    为了有效存储,根据精确的值范围,您可能希望使用位操作将键/值对存储在单个值中:例如,如果值非常小,您甚至可以使用 24 位键和 8 位的值,产生一个 32 位条目。我相信现在大多数编译器都使用 32 位或 64 位对齐,因此存储例如 32 位键和 16 位值可能仍然需要每个条目 64 位。如果瓶颈是内存总线和缓存未命中,而不是 CPU 本身,则使用简单压缩也可以提高性能。

    那么这取决于您想要执行的操作类型。存储键的最简单方法是结构的排序数组或我上面提出的组合 ley/value 条目。这是快速且非常节省空间的,但需要 O(log n) 查找。

    如果你想更花哨一点,你可以使用perfect hashing,这个想法是找到一个散列函数,为每个键生成唯一的散列值。这允许 hashmap 是一个简单的数组,它只需要比我上面提出的排序数组稍微大一点。找到一个好的散列函数应该相对较快,您可以通过使数组稍大一些并允许数组中一些未使用的字段来使其更容易。 Here 是完美散列的实现,但我自己没有使用过。

    在这两种情况下,内存消耗都是:(对数)*(每个条目的位数)位,加上使用第二种方法时存储散列函数。

    ** 编辑 **

    @FireLancer 发表评论后更新。另外,添加了一些关于压缩数组性能的文字。

    【讨论】:

    • 我没有看到您的第一个示例中的位操作对这里有什么帮助。我希望 struct Value { int x; int y; } 无论如何都会存储为 8 个连续字节。也许 key+value_1+value_2 可以说是 8 个字节而不是 12 个字节,但需要看看是否可以足够地限制值的范围。在运行时构建更好的散列函数的可能性看起来确实很有趣,但是,将通过实验来看看它对我的数据集有多密集。
    • @FireLancer 你是对的,在 C/C++ 中,只有当你不想对每个键/值使用非标准位宽时,位操作才会有所帮助(我在想 Java)。我会更新答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-29
    • 1970-01-01
    • 2013-04-29
    • 1970-01-01
    • 2017-09-30
    相关资源
    最近更新 更多