C++ 将预先保留的哈希映射(std::unordered_map)与整数键和连续数据数组(std::vector)进行比较

Posted

技术标签:

【中文标题】C++ 将预先保留的哈希映射(std::unordered_map)与整数键和连续数据数组(std::vector)进行比较【英文标题】:C++ comparing a pre-reserved hash map(std::unordered_map) with integer key and contiguous data array(std::vector) 【发布时间】:2021-01-11 16:35:03 【问题描述】:

假设使用具有int 键类型的哈希映射结构:

std::unordered_map<int, data_type> um;

另外,当知道N的元素总数(或最大)数时,可以提前构造哈希表。

um.reserve(N); // This will chainly call rehash() function...

据我所知,在这里,整数本身可以用作散列表的identity(hash)函数

同时,对于一个连续数据集(即std::vector,或者一个简单的数组),可以通过从front-的地址位移来随机访问大多数数据。

两个容器都使用int 作为访问键,如下所示:

um[1] = data_type(1); //std::unordered_map<int, data_type>
v[1] = data_type(1); //std::vector<data_type>

那么,构造出来的哈希表和std::vector在内存使用或搜索机制/性能方面有什么区别吗?

让我们把问题具体化。

如果我知道059987 3 个键肯定会被使用,但1~9986 键可能会也可能不会被使用。

如果我知道集合中没有任何键大于10000,那么使用大小为10000std::vector 将保证访问随机数据的时间复杂度为 O(1),但会浪费内存。

在这种情况下,std::unordered_map 是否会为问题提供更好的解决方案? *我的意思是,一种在保持相同级别的时间复杂度的同时尽可能节省内存的解决方案。

【问题讨论】:

比什么指标更好? @n.'pronouns'm。哦哦,对不起。在这种情况下,使用 size=10000std::vector 已经获得了最好的时间复杂度,但是对于空间复杂度来说它是最差的。因此,如果新解决方案以 7:3 左右的比例分配它们会“更好”,因为在我的领域中,内存不如速度重要。但我相信我会从任何新的问题解决方案中学到很多东西。 【参考方案1】:

另外,当元素的总(或最大)个数N已知时,可以提前构造哈希表。

um.reserve(N); // 这将链式调用 rehash() 函数...

据我所知,在这里,整数本身可以用作哈希表的标识(哈希)函数。

在两种非常不同的情况下这是正确的,并且是合理的:1)当值非常接近时可能会有一些缺失值,或者 2)当值非常随机时。在许多其他情况下,如果您不提供有意义的哈希函数,您可能会冒过度哈希表冲突的风险。

那么,构造出来的hash表和std::vector在内存使用或搜索机制/性能方面有什么区别吗?

是的。在您的.reserve(N) 之后,哈希表为至少N“buckets”分配一个连续的内存块(基本上是一个数组)。如果我们考虑 GCC 实现,N 将被四舍五入为素数。每个桶可以将一个迭代器存储到一个由pair&lt;int, data_type&gt; 节点组成的前向链表中。

因此,如果您实际上将 N 个条目放入哈希表中,那么您有...

一个包含 >= N 个 sizeof(forward-list-iterator) 大小的元素的数组 N 次内存分配 >= sizeof(pair&lt;int, data_type&gt;) + sizeof(next-pointer/iterator for forward-list)

...虽然vector 仅使用大约N * sizeof(data_type) 字节的内存:可能是哈希表使用的内存的一小部分,并且由于data_types 的所有向量内存都是连续的,你'更有可能受益于与您当前尝试访问的元素相邻的 CPU 缓存元素,这样以后访问它们的速度就会更快。

另一方面,如果您没有将很多元素放入哈希表,那么使用内存的主要内容是包含迭代器的存储桶数组,这些迭代器通常是指针的大小(例如,每个 32 位或 64 位) ,而data_type 的向量——如果你也在那里reserve(N)——已经分配了N * sizeof(data_type) 字节的内存——对于可能比哈希表大得多的data_types。尽管如此,您仍然可以经常分配虚拟内存,并且如果您没有将内存页面错误地导致它们需要物理后备内存,那么您的程序或计算机不会有任何有意义的内存使用或性能损失。 (至少对于 64 位程序,虚拟地址空间实际上是无限的)。

如果我知道 0,5,9987 3 个键肯定用过,但是 1~9986 键可能用也可能不用。

如果我知道集合中没有任何键大于 10000,那么使用大小为 10000 的 std::vector 将保证访问随机数据的时间复杂度为 O(1),但会浪费内存。

在这种情况下,std::unordered_map 是否会为问题提供更好的解决方案? *我的意思是,一种在保持相同级别的时间复杂度的同时尽可能节省内存的解决方案。

在这种情况下,如果您预先 reversed(10000) 并且 data_type 没有明显大于迭代器/指针,那么 unordered_map 在各个方面都会明显更糟。如果您不预先保留,哈希表只会为少数存储桶分配空间,并且您使用的虚拟地址空间比具有 10000 个元素的 vector 少得多(即使 data_type 是 @ 987654338@)。

【讨论】:

【参考方案2】:

一切都不一样。

unordered_map 的概念是buckets -

桶是容器内部哈希表中的一个槽,元素根据其键的哈希值分配到该槽。桶的编号从 0 到 (bucket_count-1)。

unordered_map 计算指向存储桶的键的哈希值。所需的值在该存储桶中。现在请注意,多个键可以指向单个存储桶。在您的情况下,甚至可能发生um[0]um[5]um[9987] 都位于同一个桶中!存储桶内的搜索在时间上是线性的。

在这种情况下,std::unordered_map 是否会为问题产生更好的解决方案?

如果您有稀疏数据,请使用 unordered_map 但具有适当的保留(或根本没有保留并使用默认分配策略)。如果您执行 myMap.reserve(MAX_ELEMENTS) 是没有意义的,因为这会再次导致内存浪费。

否则,使用向量。您将获得有保证的 O(1) 查找。由于它是线性的,因此它对缓存非常友好。而在 unordered_map 上,您可能会得到 O(N) 的最坏情况查找

【讨论】:

【参考方案3】:

如果你只有3个元素要打包,最好的解决方案是使用std::vector&lt;std::pair&lt;int, data_type&gt;&gt; :) 它比std::unordered_map&lt;int, data_type&gt; 占用的内存更少(实际上分配了几个vectors-buckets),并且查找性能也是最好的由于常数非常小,因此适用于少量元素。

对于较大的地图,std::vector&lt;data_type&gt;std::unordered_map&lt;int, data_type&gt; 都保证了O(1) 的复杂性,但是对于向量来说,O 中的常量隐藏要低得多,因为它不需要检查元素桶中的其他元素。我建议始终首选矢量,除非您缺少适合它的内存,在这种情况下,您可以使用 unordered_map 通过牺牲一点性能来节省内存。

【讨论】:

以上是关于C++ 将预先保留的哈希映射(std::unordered_map)与整数键和连续数据数组(std::vector)进行比较的主要内容,如果未能解决你的问题,请参考以下文章

是否可以在 Perl 中保留哈希表的大小?

超高性能 C/C++ 哈希映射(表、字典)[关闭]

以整数为键并映射到 void 指针的 C 映射/哈希表

C++哈希表/散列表

C ++中的哈希表?

如何使用 c++ 将字符串散列为 int?