我生成了大量的类实例并将它们存储在std::unordered_set
中。我已经定义了一个哈希函数和一个相等关系,到目前为止一切正常 - 我用unordered_set::insert
插入10000个实例,我可以用unordered_set::find
找到它们。所有对象都没有损坏,并且没有关于内存损坏或任何其他问题的提示。
但是,当我继续插入时,第32769次插入失败 - 它不会抛出,但它会返回迭代器为== nullptr
(0x00000000)的对。 insert
定义为:
pair<iterator, bool> insert(const value_type& Val);
通常情况下,*iterator
是我插入的密钥,而bool是true
。
如果我(在错误之后)尝试找到对象,则是在集合中;如果我再次尝试插入它,它会告诉我它已经存在;所以插入似乎工作正常。返回的值只有pair<nullptr,true>
而不是pair<iterator,bool>
请注意,如果我手动填充迭代器并在调试器中继续,则在65536之后的第一次插入,然后在131072等处再次发生相同的问题(因此对于2 ^ 15 + 1,2 ^ 16 + 1,2 ^ 17 + 1,...) - 但不是3 * 32768 + 1,等等。
对我而言,这似乎有些short
溢出。也许我的哈希真的很糟糕并且导致铲斗的不均匀填充,并且在32768它用尽了铲斗?在google搜索时我找不到任何关于这种限制的更详细信息,而且我对平衡树或内部的任何内容都知之甚少。
仍然,std库代码应该能够处理坏散列,我理解它是否变得缓慢和低效,但它不应该失败。
问题:为什么2 ^ 15th + 1,2 ^ 16th + 1等插入失败,我该如何避免呢?
这是在Microsoft Visual Studio 2017 V15.7.1(最新版本,截至2018-05-15)。编译器设置为使用C ++ 2017规则,但我怀疑它是否会产生任何影响 我无法粘贴完整的代码以获得最小的可行解决方案,因为对象生成在多个类和方法中很复杂,并且有几百行代码,生成的哈希值显然取决于对象的细节,并且不易重现虚拟代码。
###一天后更新### :(我不能把这个放回答,因为q被搁置了)
经过对标准库的大量调试(包括许多令人头疼的事),@ JamesPoag的回答结果指出了正确的事情。
插入n
后,我得到:
n load_factor max_load_factor bucket_count max_bucket_count
32766 0.999938965 1.00000000 32768 536870911 (=2^29-1)
32767 0.999969482 1.00000000 32768 536870911
32768 1.000000000 1.00000000 32768 536870911
32769 0.500000000 1.00000000 65536 536870911
毫不奇怪,在32768次插入后,负载系数已达到最大值。第32769次插入在内部方法_Check_Size:
内触发重写到更大的表void _Check_size()
{ // grow table as needed
if (max_load_factor() < load_factor())
{ // rehash to bigger table
size_type _Newsize = bucket_count();
if (_Newsize < 512)
_Newsize *= 8; // multiply by 8
else if (_Newsize < _Vec.max_size() / 2)
_Newsize *= 2; // multiply safely by 2
_Init(_Newsize);
_Reinsert();
}
}
最后,调用_Reinsert()
并将所有32769个键填充到新存储桶中,并相应地设置所有_next
和_prev
指针。这很好。
但是,调用这两个代码的代码如下所示(Plist
是我的集的名称,此代码从模板生成):
_Insert_bucket(_Plist, _Where, _Bucket);
_TRY_BEGIN
_Check_size();
_CATCH_ALL
erase(_Make_iter(_Plist));
_RERAISE;
_CATCH_END
return (_Pairib(_Make_iter(_Plist), true));
}
关键点在最后一行 - _Plist用于构建该对,但它保存了一个现在已经死的指针_next
,因为所有存储桶的地址都在_Check_size()
中重建,有些行更早。
我认为这是std库中的一个错误 - 在这里它需要在新集合中找到_Plist
,它看起来一样,但是有一个有效的_next
指针。
在关键insert
之前,一个简单的“修复”(已验证可以正常工作)展开设置:
if (mySet.size() == mySet.bucket_count()) mySet.rehash(mySet.bucket_count() * 2);
。
###进一步更新:### 我一直在尝试(16个多小时)来生成一个最小代码来重现问题,但我还没有。我将尝试记录现有大代码的实际计算哈希值 我发现的一件事是,在插入和重新插入之间,其中一个键的一个哈希值发生了(无意中)。这个可能是根本原因;如果我在插入物之外移动重复,问题就会消失 我不确定是否有一个哈希值必须保持不变的规则,但它可能有意义,你怎么能再次找到密钥。
答案 0 :(得分:3)
我将一些简单的代码插入到godbolt.org中,看看输出是什么,但没有任何东西向我跳出来。
我怀疑是插入了Value并且创建了迭代器,但是插入超过了max_load_factor并触发了重新散列。在Rehash上,以前的迭代器无效。在这种情况下,返回迭代器可能会被清零(或从不设置)(再次在反汇编中找不到它)。
在违规插入之前和之后检查load_value(),max_load_value()和bucket_count()。
答案 1 :(得分:0)
[这是一个自我回答]
这个问题不在标准库中,假设它毕竟在我的代码中(很少有惊喜)。发生了什么:
我将复杂对象插入到unordered_set中,并从对象计算哈希值。假设对象1具有散列H1,对象2具有散列H2,等等 接下来,我暂时修改插入的对象,克隆它,将克隆插入 unordered_set ,然后撤消修改。但是,如果 insert 触发集合的重组(发生在2 ^ 15,2 ^ 16等),则会重新计算所有现有对象的哈希值。 由于对象1当前是“临时修改”的,因此其散列不会返回H1,而是不同。这会弄乱集合的内部结构,并最终返回一个无效的迭代器。伪代码:
{{1}}
如果我在插入之外移动重新散列,或者在关键插入之前撤消对象的修改(因此哈希再次正确),问题就会消失。
还有其他几种方法可以避免这个问题(在修改之前克隆,在对象上保存哈希值,不要重新计算等)。
核心课程:哈希计算必须稳定。您无法修改集合或映射中的对象,如果它更改了计算的散列 - 集合或映射可能会在意外的时间点触发重新散列。