为什么32769插入在std :: unordered_set中失败?

时间:2018-05-18 01:19:50

标签: c++ c++17 unordered-set

我生成了大量的类实例并将它们存储在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个多小时)来生成一个最小代码来重现问题,但我还没有。我将尝试记录现有大代码的实际计算哈希值 我发现的一件事是,在插入和重新插入之间,其中一个键的一个哈希值发生了(无意中)。这个可能是根本原因;如果我在插入物之外移动重复,问题就会消失 我不确定是否有一个哈希值必须保持不变的规则,但它可能有意义,你怎么能再次找到密钥。

2 个答案:

答案 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}}

如果我在插入之外移动重新散列,或者在关键插入之前撤消对象的修改(因此哈希再次正确),问题就会消失。
还有其他几种方法可以避免这个问题(在
修改之前克隆,在对象上保存哈希值,不要重新计算等)。

核心课程:哈希计算必须稳定。您无法修改集合或映射中的对象,如果它更改了计算的散列 - 集合或映射可能会在意外的时间点触发重新散列。