【问题标题】:efficiently initialize unordered_map with large dataset of integer pairs使用大型整数对数据集有效地初始化 unordered_map
【发布时间】:2016-01-11 22:52:39
【问题描述】:

我有一个巨大的数组(例如,ParticleId[]),由 唯一 个整数(代表粒子 ID)组成,它们以随机顺序存储在内存中。我需要构建一个哈希表来将每个 ID 映射到它在数组中的位置,即从 ID 到索引。 ID 不一定是连续整数,因此简单的查找数组不是一个好的解决方案。

我目前正在使用 c++11 的 unordered_map 容器来实现这一点。使用循环初始化地图:

unordered_map <ParticleId_t, ParticleIndex_t> ParticleHash;
ParticleHash.rehash(NumberOfParticles);
ParticleHash.reserve(NumberOfParticles);
for(ParticleIndex_t i=0;i<NumberOfParticles;i++)
  ParticleHash[ParticleId[i]]=i;

ParticleId_tParticleIndex_t 只是类型定义的整数。 NumberOfParticles 可以很大(例如,1e9)。就哈希表而言,ParticleId[] 数组和NumberOfParticlesconst

目前,如上所述构建unordered_map 需要花费大量时间。我的问题是:

  1. unordered_map 是这个问题的最佳选择吗?
    • map 的初始化速度是否会更快,尽管它在查找时可能效率不高?
  2. 是否可以加快初始化速度?
    • 使用ParticleHash.insert() 是否比使用ParticleHash[]= 快得多?或任何其他功能?
    • 鉴于我的键是唯一整数,有没有办法优化映射以及插入?

我正在考虑使用英特尔concurrent_unordered_map 来并行化它。但是,这会引入对英特尔 TBB 库的依赖,如果可能的话,我想避免这种情况。有没有使用原生 STL 容器的简单解决方案?

更新:

现在我已经恢复到一个普通的排序索引表并依靠bsearch 进行查找。至少表的初始化现在快了 20 倍,并且可以轻松并行化。

【问题讨论】:

  • 看看这个——包括关于在构造函数中指定桶大小的评论:stackoverflow.com/questions/11614106/…
  • 使用std::map 你可以传递一个提示迭代器来加速插入。如果您知道下一个键是地图中的最后一个键,则可以传递 end 迭代器作为我相信的提示。我不知道这是否会比无序地图更快。还要考虑 boost 提供的一些 flat_map 数据结构。
  • @JerryJeremiah:啊,我使用的是 gcc4.7.2。也许这就是原因。在确认之前我必须找到另一个编译器..
  • @NeilKirk:地图中的最后一个是指最后插入的还是有序键中的最后一个?如果是后者,我认为我最好选择自己的实现,先排序,然后是 binary_search
  • 从 gcc4.7.2 切换到 4.8.1 的速度提高了 2 倍。Turing 优化 -O3 的速度提高了 2 倍。

标签: c++ performance hashmap unordered-map scientific-computing


【解决方案1】:

似乎构建查找表的应用程序受内存限制,而不是 CPU 限制。这可以通过分析应用程序的原型来验证。这个答案的其余部分假设这是真的。

构建查找表的过程正在获取输入数据的全局视图,这可能会导致大量内存换入/换出磁盘。

如果是这种情况,解决方案是一次处理较小内存块的替代算法。 假设有 100 万个整数。当前进程此时可能正在插入接近 1 的哈希表的低端,而下一刻它可能正在插入接近 100 万的高端。这会导致大量交换。

另一种方法是通过一次处理较小的数据集块来避免交换。我们可以借鉴桶/基数排序的想法。在这种方法中,构建查找表的步骤将由排序步骤代替。桶/基数排序应该在线性时间内运行。数据集中的所有整数都是唯一的这一事实是使用这些排序算法的另一个原因。 如果可以将线性时间排序和交换的最小化结合起来,那可能会提高性能。

【讨论】:

  • 我的案例实际上是不受内存限制的,因为我在一台内存足够多的超级计算机上运行。
【解决方案2】:

我不认为你可以用这个做很多事情,但这里有一些事情可以尝试。

首先,由于您调用的是realloc,因此您无需调用rehash

insert 可能比operator[] 快​​,因为operator[] 将调用insert 以使用默认值将元素添加到地图,然后将您的值分配给新插入的元素,但优化器可能能够以消除额外的工作。

仅仅因为 keys 是唯一的,这些键的散列值可能不是因为我认为语言规范不要求整数散列返回该整数(描述散列的部分反正模板没说)。

'map' 的初始化可能会更慢,因为它必须在您插入内容时不断重新平衡树,并且查找会更慢。如果您的ParticleID 向量可以重新排列,则可以使用map 的一种替代方法是对向量​​进行排序,然后执行binary_search 以查找ID 的位置并计算索引。但它的性能与map 相似,并且需要重新排列向量。

如果您决定尝试concurrent_unordered_map,由于线程之间的所有内存争用,在 3 或 4 个线程之后您可能不会看到太大的改进。

【讨论】:

  • 我在reserve之前做了rehash明确设置桶的数量,希望它可以帮助将冲突减少到最小。很高兴知道默认哈希函数不需要返回同一个整数。也许我应该通过我自己的哈希器。我的旧实现确实是排序 ID 上的binary_search。很遗憾知道专用的 hashmap 容器并没有做得更好..
  • 您能否对应用进行分析以查看哪些部分花费的时间最多?考虑到超级计算机的 CPU、内存和其他资源,我想知道什么是瓶颈。操作系统级别是否有任何配额,是否已为该应用分配了足够的 CPU/内存?
【解决方案3】:

鉴于“以随机顺序存储的大量唯一整数” - 是否已经有任何东西取决于该随机顺序?如果没有,只需对唯一整数数组进行就地排序,并将唯一整数映射到索引,您可以在数组中执行 std::lower_bound

如果需要保留大数组的预先存在的顺序,但您在填充该数组后作为一次性步骤构建索引(如您的说明性代码所做的那样),您可以创建一个同样巨大的ParticleId*std::sort 数组基于指向的元素(您需要自定义&lt; 指向值的比较);之后您可以使用std::lower_bound 与相同的&lt; 比较来快速找到特定ParticleId 的巨大数组中的索引。

上述连续数组方法通过以缓存友好的方式使用连续内存极大地提高了性能和内存使用率。

只有当您有大量新的ParticleIds 在您需要能够搜索的时间进入或被删除时,您才需要考虑std::unordered_map

【讨论】:

  • 数据排序不是最优的,因为每个粒子以相同的顺序关联的数据要多得多。创建另一个数组基本上是我在尝试 unordered_map 之前所做的。
  • @Kambrian: “创建另一个数组本质上就是我在做的事情” - 它还不够快吗?您是否使用指向第一个数组的指针?无论如何,如果你想追求一个哈希表,你会好得多 - 对于这种特定的使用模式并且给定 sizeof(ParticleId) 很小 - 编写或找到你自己的使用开放寻址/封闭哈希;对于类似的使用情况,我倾向于发现在我的硬件上比 unordered_map 快一个数量级,并且您可以显着减少内存开销(尽管连续数组总是更有效)。
  • 我创建了一个 (ID, Index) 对数组,然后按 ID 对其进行排序。然后使用二进制搜索搜索该数组以找到每个 ID 查询的索引。我天真地期待一个专用的哈希表可能会做比这更聪明的事情。但是发现不是针对这个特殊应用也不错。谢谢!
猜你喜欢
  • 2013-01-25
  • 2017-03-22
  • 2014-05-24
  • 2023-03-06
  • 1970-01-01
  • 1970-01-01
  • 2012-06-16
  • 1970-01-01
  • 2020-10-25
相关资源
最近更新 更多