C ++映射插入和查找性能和存储开销

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C ++映射插入和查找性能和存储开销相关的知识,希望对你有一定的参考价值。

我想将integer键的映射存储到内存中的float值。

我有大约1.3亿个键(因此,有1.3亿个键)。

我的重点是查找性能 - 我必须进行许多,数百万次查找。

C ++ STL库有一个map类,用于这种关联数组。我有几个关于map的问题。

map对于上述大小的数据集的存储开销是多少?一般来说,存储开销如何与map一起扩展?

看起来map的底层数据结构是一个红黑色的平衡二叉树。这听起来像现实世界performance这是O(log n)插入和检索。

它提到O(1)暗示插入。我的输入是预先排序的,所以我相信我应该能够提供插入事件的提示。我如何使用here列出的方法提供此提示?

是否有一个提供更好查找性能的STL容器?

是否有其他公开可用的开源框架,其关联数组类使用的底层数据结构比STL map表现更好?

如果编写我自己的容器类可以提供更好的查找性能,我可以研究哪些数据结构?

我正在使用GCC 4执行此任务,在Linux或Mac OS X下运行。

如果这些都是愚蠢的问题,我会提前道歉。感谢您的意见。

答案

鉴于你所说的,我会非常认真地使用std::vector<pair<int, float> >,并使用std::lower_boundstd::upper_bound和/或std::equal_range来查找值。

虽然std::map的确切开销可以(并且确实)变化,但是通常消耗额外的内存并且比向量中的二进制搜索更慢地查找值的问题很少或根本没有。正如您所指出的,它通常(并且几乎不可避免地)实现为某种平衡树,这会对指针和平衡信息施加开销,并且通常意味着每个节点也单独分配。由于您的节点非常小(通常为8个字节),因此额外数据可能至少与您实际存储的数据一样多(即至少100%的开销)。单独分配通常意味着参考的局部性较差,这导致缓存使用率较低。

std::map的大多数实现使用红黑树。如果您打算使用std::map,使用AVL树的实现可能更适合您的目的 - AVL树对平衡的约束稍微严格。这样可以稍微加快查找速度,但代价是插入和删除稍慢(因为它必须更频繁地重新平衡以保持对“平衡”的更严格的解释)。但是,只要您的数据在使用过程中保持不变,std::vector几乎肯定会更好。

另一个值得注意的可能性:如果你的密钥至少是相当均匀分布的,你可能想尝试使用插值而不是二等分来查找。即,不是始终从向量的中间开始,而是进行线性插值以猜测最有可能的查找起始点。当然,如果您的键遵循一些已知的非线性分布,则可以使用匹配插值。

假设键被合理地均匀分布(或者至少遵循一些可以插值的可预测模式),则插值搜索具有O(log log N)的复杂度。对于1.3亿个密钥,可以使用大约4个探针来查找项目。要做得比使用(正常/非完美)散列要好得多,你需要一个好的算法,并且需要将表中的负载因子保持在相当低的水平(通常约为75%左右 - 即你需要允许表格中有3200万个额外(空)点,以提高从四个探针到三个探测器的预期复杂性。我可能只是老式的,但这让我感觉很多额外的存储空间可用于如此小的速度提升。

OTOH,确实这几乎是完美散列的理想情况 - 该集合是提前知道的,并且密钥非常小(重要的是,因为散列在密钥大小上通常是线性的)。即便如此,除非键分布相当不均匀,否则我不会指望任何巨大的改进 - 完美的哈希函数通常(通常是?)相当复杂。

另一答案

假设您不需要在向量中间进行插入,那么向量绝对会在这里杀死地图。我编写了一个自定义分配器来跟踪内存使用情况,以下是Visual Studio 2005中的结果:

std::map<int, float>:

1.3 million insertions
Total memory allocated: 29,859 KB
Total blocks allocated: 1,274,001
Total time: 17.5 seconds

std::vector<std::pair<int, float> >:

1.3 million insertions
Total memory allocated: 12,303 KB
Total blocks allocated: 1
Total time: 0.88 seconds

std :: map使用了两倍以上的存储空间,并且插入所有项目需要花费20倍的时间。

另一答案

如果您的输入已排序,则应尝试仅使用向量和二进制搜索(即lower_bound())。这可能证明是充分的(它也是O(log n))。根据键的分布和使用的散列函数,hash_map也可以起作用。我认为这是gcc中的tr1::unordered_map

另一答案

大多数编译器都附带一个非标准(但工作)hash_map(或unordered_map),可能会更快。它来自C ++ 0x(在tr1中)并且它(也一如既往)已经在boost中。

海湾合作委员会也做了,但我还没有完成C ++ 12年......,但它应该仍然存在于某个地方。

另一答案

您可以查看std :: tr1 :: unordered_map。

但是如果你有32位无符号整数键(4294967296个可能的值)和1.3亿个不同的键,那么你应该编写自己的容器来优化这个任务。特别是如果1.3亿关键案例是通常的情况(并且不仅是罕见的最大值)。

4294967296/130000000 = 33,因此在您的数据中使用整个空间中的每个第33个数字。

例如,您可以将键范围划分为固定大小的分区。如果密钥分布相当均匀,则应将密钥空间划分为例如256个存储桶,甚至32个存储桶,取决于只存储少量值时要浪费多少存储空间。

例如,给你一个想法:

#define BUCKET_SIZE  256
#define BUCKET_SIZE_SHIFT  8
struct Bucket {
  uint32_t key;
  float value;
  Bucket* pNext;
};

Bucket data[ 4294967296 / BUCKET_SIZE ];

Bucket* find( uint32_t key ) {
  uint32_t bucket_index = key / BUCKET_SIZE;
  // or faster: uint32_t bucket_index = key >> BUCKET_SIZE_SHIFT;
  Bucket* pBucket = &data[ bucket_index ];
  while( pBucket ) {
    if( pBucket->key == key ) return pBucket;
    pBucket = pBucket->pNext;
  }
  return NULL;
}
另一答案

如果您的钥匙不变,您可以考虑使用perfect hash function作为标准容器的替代品。

我不知道你会遇到这个大小的数据集会遇到什么障碍,但是可能值得花一些时间进行试验。

另一答案

考虑到使用了大量内存,您还必须考虑搜索中的任何内存访问都会导致内存缓存错误。

在这方面,作为第一层的小散列图和用于桶的排序向量的混合解可能是最好的。

我们的想法是将哈希表索引保留在缓存内存中,并搜索较小的已排序容器以减少缓存故障的数量。

另一答案

作为有关查找性能问题的部分答案,您必须考虑插入模式。您注意到std::map使用红黑树作为对冲,将精心排序的插入线性化为链表。因此,尽管存在异常的插入顺序,但这样的树提供O(log n)查找时间。但是,您需要为插入,删除和遍历性能支付费用,以及丢失重复读取“附近”数据的参考位置。

如果您可以为您的密钥类型(整数,您说)调整一个可以排除冲突的哈希函数,则哈希表可以提供更快的查找。如果您的数据集是固定的,这样您可以加载一次并且之后只读取它,您可以使用整数和浮点数的并行数组,并使用std::lower_bound通过二进制搜索找到您的匹配项。正确地对并行数组进行排序将是一件苦差事,如果你的键与它们的相应值分开,但是你比存储std::pair的数组更喜欢存储和引用的局部性。

以上是关于C ++映射插入和查找性能和存储开销的主要内容,如果未能解决你的问题,请参考以下文章

C ++ - 类方法更改成员变量,但不在main中

Dapper-小型ORM之王(C#.NET)

哈希表之四查找及分析

C++/C# 互操作中的内存映射和 P/Invoke 性能

替换为范围和哈希映射

红黑树(Red Black Tree)