最快的C++ map?

25

如果我错了,请纠正我,但 std::map 是一种有序的映射容器,因此每次插入一个值,map 都会使用一种算法来内部排序其项,这需要一些时间。

我的应用程序以恒定的间隔获取有关某些项目的信息。

这个应用程序保留一个定义如下的映射:

::std::map<DWORD, myItem*>

一开始所有的物品都被应用程序视为“新”的。一个“Item”对象被分配并添加到这个map中,将它的id和指向它的指针关联起来。

当它不是一个“新”的物品(只是对此对象的更新)时,我的应用程序应该在map中使用给定的id查找对象并进行更新。

大多数情况下我会得到更新。

我的问题是:
是否有任何更快的map实现,或者我应该继续使用这个?
我最好使用unordered_map吗?


4
我的经验是,在我使用的情况下,unordered_map 的速度大约是 map 的两倍。这很容易测试,因为您的 map 不需要自定义哈希函数 - 只需将 unordered_map 替换为 map,并计时生成的代码即可。 - anon
5
您的应用程序速度很慢吗?您已经对其进行过剖析了吗?地图性能是否真的是最重要的因素?您通常使用多大的数据集?它适合在内存中还是您的应用程序会严重使用交换空间(swap)? - Tomek Szpakowicz
只需使用最合适的容器。我认为大多数情况下,当你从一个东西映射到另一个东西时,你实际上并不关心这个映射的顺序。这将使unordered_map更加合适。只有在顺序很重要的情况下才应该使用map。不幸的是,unordered_map被引入得太晚了。但像@Neil所说,我也通过unordered_map获得了极大的性能提升,因为我只进行查找并且不关心顺序。但是,在进行大量插入和删除操作时,map往往会胜出。 - GManNickG
鉴于std::map和std::unordered map很容易互换,因此在使用真实数据时尝试两者并确定哪个在实际情况下更快速应该是微不足道的。理论上,std::unordered map通常用于查找和插入的摊销常数时间,但除非您的表非常大,否则常数可能是相关的。 - Adrian McCarthy
3个回答

43

我是更好使用unordered_map吗?

可能。

std:map提供O(log n)的一致性性能,因为它需要实现为平衡树。但std:unordered_map将被实现为哈希表,这可能会给您O(1)的性能(良好的哈希函数和跨哈希桶分配键),但它可能是O(n)(所有内容都在一个哈希桶中并退化为列表)。通常可以期望介于这些极端之间的某些东西。

因此,您可以始终拥有合理的性能(O(log n))或者需要确保所有内容都适合哈希以获得良好的性能。

与任何此类问题一样:在承诺采用一种方法之前,您需要进行测量。除非您的数据集很大,否则您可能会发现没有显着的区别。


那么你的意思是没有指导线告诉我们吗?嗯,与地图中的项目相关联的ID确实会有所不同,但如果您只使用提供项目信息的一个来源,则ID的分布非常接近。 - Poni
16
在这个语境中,“Large”意味着真正的、天文数字般的大。当比较O(1)和O(logn)算法时,常数因素往往会支配可以一次性存储在内存中的任何数据集的运行时间。如果性能真的很重要,一定要针对代表性案例进行实际测量。 - Alan
1
一个(正确实现的)哈希表具有平摊常数时间复杂度保证,这在99%的情况下已经足够好了。 - Staffan
1
@Poni:实际上,所使用的关键字类型(在您的情况下为DWORD)会影响最佳选择。计算长度为m的字符串的哈希需要Θ(m)(大Theta)时间,而用于常规映射的字符串比较具有O(m)(大O)的复杂度。也就是说,当您计算“Hello”的哈希时,必须查看所有字符。但例如在比较“Hello”和“World”时,您只需要查看一个字符! - Staffan
根据使用的字符串,可能允许对字符串中不是每个字符进行哈希。例如,在C实现中,如果前31个字符相同,则认为标识符相等。显然,在这种情况下,标识符哈希可以仅哈希前31个字符。 - MSalters
显示剩余3条评论

11

重要警告:除非您已经测量过(而您的问题表明您没有),映射性能对应用程序性能产生了实质性影响(大量时间用于搜索和更新映射),否则不要费力去使其更快。 坚持使用 std::map(或 std::unordered_map 或任何可用的 hash_map 实现)。 将应用程序加速1%可能并不值得努力。 相反,让它无错误。

回应Richard的答案:使用实际数据和真实类别测试使用不同的映射实现来衡量性能。

一些额外的注意事项:

  • 了解预期成本的差异(哈希映射通常较低),最坏情况成本(平衡二叉树为O(logn),但如果插入触发哈希数组的重新分配,则哈希映射的成本要高得多),以及平摊成本(总成本除以操作或元素数;取决于新元素和现有元素的比率)。您需要找出在您的情况下哪个更具约束力。例如,如果您需要遵守非常低的延迟限制,则重新分配哈希映射可能太多。

  • 找出真正的瓶颈所在。可能是在映射中搜索的成本与例如IO成本相比微不足道。

  • 尝试更专业的映射实现。例如,如果您了解有关映射键的更多信息,则可以获得很多收益。通用映射实现作者没有此类知识。

在您的示例(32位无符号整数密钥强烈聚类,例如按顺序分配)中,可以使用基于基数的方法。 非常简单的示例(将其视为说明而不是可用食谱):

Item *sentinel[65536];  // sentinel page, initialized to NULLs.
Item (*pages[65536])[65536];  // list of pages,
                              // initialized so every element points to sentinel

那么搜索就很简单:

Item *value = pages[index >> 16][index & 0xFFFF];

当您需要设置新值时:

if (pages[index >> 16] == sentinel) {
  pages[index >> 16] = allocate_new_null_filled_page();
}
pages[index >> 16][index & 0xFFFF] = value;
  • 优化你的地图实现。

    • 例如,每个hash_map都喜欢预先知道大约有多少元素。这可以帮助避免哈希表不必要的重新分配和(可能)所有键的重新哈希。

    • 对于上面我提到的专业例子,你肯定会尝试不同的页面大小,或者三级版本。

    • 常见的优化是提供专门的内存分配器,以避免多次分配小对象。


考虑使用数组而不是映射,但这种方法会限制我只能处理一定数量的项目和它们的ID,因为这些ID并非按顺序分配。除此之外,你提供了一些新闻,我没有真正考虑过在空中分配它们,这似乎非常正确。谢谢! - Poni
@Poni:认真的吗?只需要使用new并用零进行memset即可。也许现在应该使用std::map(或std::unordered_map)... - Tomek Szpakowicz
是的,你抓住了我。我说的是“new”语句,就像“new .....”一样。不幸的是,这让我感到困惑。 - Poni

3
每当您插入或删除项时,内存分配/释放的成本都很高。相反,您可以使用像这样的分配器:https://github.com/moya-lang/Allocator,据作者说可以加速std::map两倍,但我发现它对于其他STL容器来说速度更快。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接