C++ - 为啥 boost::hash_combine 是组合散列值的最佳方式?

Posted

技术标签:

【中文标题】C++ - 为啥 boost::hash_combine 是组合散列值的最佳方式?【英文标题】:C++ - Why is boost::hash_combine the best way to combine hash-values?C++ - 为什么 boost::hash_combine 是组合散列值的最佳方式? 【发布时间】:2016-06-29 09:33:24 【问题描述】:

我在其他帖子中读到这似乎是组合散列值的最佳方式。有人可以分解一下并解释为什么这是最好的方法吗?

template <class T>
inline void hash_combine(std::size_t& seed, const T& v)

    std::hash<T> hasher;
    seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);

编辑:另一个问题只是询问幻数,但我想了解整个功能,而不仅仅是这一部分。

【问题讨论】:

Magic number in boost::hash_combine的可能重复 所以:所以“随机”包含这个数字会改变种子的每一位;正如您所说,这意味着连续值将相距甚远。包括旧种子的移位版本可确保即使 hash_value() 具有相当小的值范围,差异也会很快扩散到所有位。;接受的答案对您不起作用? 已加载问题。这不是最好的方法。这是一个通用的。 散列类型聚合的另一种方法:视频:youtube.com/watch?v=Njjp_MJsgt8&feature=youtu.be 论文:open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html 实现:github.com/HowardHinnant/hash_append 另一种方法是重载一个“系列”模板,该模板迭代 UDT (github.com/ywkaras/trafficserver/blob/fnv1a/lib/ts/Series.h) 中的所有数据。一旦为类型定义了系列模板,散列模板函数将使用该类型 (github.com/ywkaras/trafficserver/blob/fnv1a/lib/ts/fnv1aHash.h)。 (这里是使用 catch.hpp github.com/ywkaras/trafficserver/blob/fnv1a/lib/ts/unit-tests/… 对该代码进行的快速单元测试。) 【参考方案1】:

“最好”是有争议的。

“好”,甚至“非常好”,至少在表面上,很容易。

seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);

我们假设seedhasher 或此算法的先前结果。

^=表示左边的位和右边的位都改变结果的位。

hasher(v) 被认为是v 上的一个不错的哈希值。但剩下的就是防御,以防它不是一个像样的哈希。

0x9e3779b9 是一个 32 位值(如果 size_t 可以说是 64 位,它可以扩展到 64 位),其中包含半个 0 和半个 1。它基本上是通过将特定的无理常数近似为以 2 为底的定点值来完成的 0 和 1 的随机序列。这有助于确保如果哈希返回错误值,我们的输出中仍然会出现 1 和 0 的污点。

(seed&lt;&lt;6) + (seed&gt;&gt;2) 是传入种子的一点洗牌。

想象一下 0x 常量丢失了。想象一下,哈希器为几乎每个传入的v 返回常量0x01000。现在,种子的每一位都在哈希的下一次迭代中展开,在此期间它再次展开。

seed ^= (seed&lt;&lt;6) + (seed&gt;&gt;2)0x00001000 在一次迭代后变为0x00041400。然后0x00859500。当您重复该操作时,任何设置的位都会“涂抹”在输出位上。最终左右位碰撞,进位将设置位从“偶数位置”移动到“奇数位置”。

随着组合操作在种子操作上递归,依赖于输入种子值的位会以相对快速且复杂的方式增长。添加原因会带来更多影响。 0x 常量添加了一堆伪随机位,使得无聊的哈希值在组合后占据了更多位的哈希空间。

由于加法(结合"dog""god" 的哈希值会产生不同的结果),它是不对称的,它处理无聊的哈希值(将字符映射到它们的 ascii 值,这只涉及旋转少量位)。而且,速度相当快。

在其他情况下,加密强度较高的较慢哈希组合可能会更好。我天真地认为,使移位成为偶数和奇数移位的组合可能是一个好主意(但也许加法,它从奇数位移动偶数位,使问题变得不那么成问题:在 3 次迭代之后,传入的孤种子位会碰撞和相加并导致进位)。

这种分析的缺点是只需要一个错误就可以使哈希函数变得非常糟糕。指出所有美好的事物并没有多大帮助。所以现在让它变得更好的另一件事是它相当有名并且在一个开源存储库中,我还没有听到有人指出它为什么不好。

【讨论】:

有没有一种简单的方法可以看出seed -&gt; (seed&lt;&lt;6) + (seed&gt;&gt;2) 是双射的? 没有简单的方法可以看出所提到的变换是双射的,因为它不是。在 16 位域中有 192 个结肠。在 24 位域中 48960... 假设种子和结果都是相同的位大小。 @MartinR hash_combine(x,0) 的 32 位值有 1346300007 次冲突。 @WolfgangBrehm:我的评论提到了这个答案中的声明“任何种子输入都是双射的”,同时已被删除。 @MartinR 好吧,无论如何我刚刚计算出我的答案,花了大约一个小时,虽然你可能会感兴趣:D【参考方案2】:

这不是最好的,令我惊讶的是它甚至不是特别好。主要问题是糟糕的分布,这并不是 boost::hash_combine 本身的错,而是与像 std::hash 这样的糟糕分布的散列一起使用,这种散列最常使用标识函数实现。

图 2:两个随机 32 位数字之一的单个位更改对 boost::hash_combine 结果的影响。 x 轴上是输入位(两次 32,首先是新哈希,然后是旧种子),y 轴上是输出位。颜色表示依赖程度。

为了证明事情会变得多么糟糕,当按预期使用hash_combinestd::hash 时,32x32 网格上的点 (x,y) 的碰撞:

# hash_combine(hash_combine(0,x₀),y₀)=hash_combine(hash_combine(0,x₁),y₁)
# hash      x₀   y₀  x₁  y₁
3449074105  6   30   8  15
3449074104  6   31   8  16
3449074107  6   28   8  17
3449074106  6   29   8  18
3449074109  6   26   8  19
3449074108  6   27   8  20
3449074111  6   24   8  21
3449074110  6   25   8  22

对于分布良好的哈希值,统计上应该没有。可以使 hash_combine 级联更多(例如,通过使用多个展开的异或移位)并更好地保留熵(例如,使用位旋转而不是位移位)。但实际上你应该做的是首先使用good hash function,然后如果哈希编码序列中的位置,那么简单的异或就足以组合种子和哈希。为了便于实施,以下哈希不对位置进行编码。要使hash_combine 不可交换,任何非交换和双射操作就足够了。我选择了非对称二元旋转,因为它便宜。

#include <limits>
#include <cstdint>

template<typename T>
T xorshift(const T& n,int i)
  return n^(n>>i);


// a hash function with another name as to not confuse with std::hash
uint32_t distribute(const uint32_t& n)
  uint32_t p = 0x55555555ul; // pattern of alternating 0 and 1
  uint32_t c = 3423571495ul; // random uneven integer constant; 
  return c*xorshift(p*xorshift(n,16),16);


// a hash function with another name as to not confuse with std::hash
uint64_t distribute(const uint64_t& n)
  uint64_t p = 0x5555555555555555ull; // pattern of alternating 0 and 1
  uint64_t c = 17316035218449499591ull;// random uneven integer constant; 
  return c*xorshift(p*xorshift(n,32),32);


// if c++20 rotl is not available:
template <typename T,typename S>
typename std::enable_if<std::is_unsigned<T>::value,T>::type
constexpr rotl(const T n, const S i)
  const T m = (std::numeric_limits<T>::digits-1);
  const T c = i&m;
  return (n<<c)|(n>>((T(0)-c)&m)); // this is usually recognized by the compiler to mean rotation, also c++20 now gives us rotl directly


// call this function with the old seed and the new key to be hashed and combined into the new seed value, respectively the final hash
template <class T>
inline size_t hash_combine(std::size_t& seed, const T& v)

    return rotl(seed,std::numeric_limits<size_t>::digits/3) ^ distribute(std::hash<T>(v));

种子在组合之前旋转一次,以使计算哈希的顺序相关。

来自boosthash_combine 需要更少的两个操作,更重要的是不需要乘法,实际上它快了大约 5 倍,但是在我的机器上每个哈希大约 2 cyles,所提出的解决方案仍然非常快并且很快就会得到回报当用于哈希表时。在 1024x1024 网格上有 118 次碰撞(boostshash_combine + std::hash 的碰撞次数为 982017),这与分布良好的哈希函数的预期一样多,这就是我们所能要求的。

现在即使与良好的散列函数 boost::hash_combine 结合使用也不理想。如果所有熵在某个时候都在种子中,其中一些会丢失。 boost::hash_combine(x,0) 有 2948667289 个不同的结果,但应该有 4294967296。

总之,他们试图创建一个哈希函数,它可以同时进行组合和级联,而且速度很快,但最终得到的结果是两者都做得很好,不会立即被认为是坏的。但速度很快。

【讨论】:

好答案。这让我怀疑为什么每个人都在使用这个功能而不是更好的东西,所以我花了几个小时在一个洞里研究这个。实际上,std::hash 并没有将好的位雪崩列为实现需要具备的属性,因此虽然您认为这是一个一般的差散列函数是正确的,但它实际上完全满足了 std::hash 设置的要求。例如,size_t 的 std::hash 实现通常只是恒等函数——这很好。 是的,所以很明显 boost hash_combine 是一个糟糕的哈希函数,因为你可以找到这些微不足道的冲突,而且它显然不满足 std::hash operator() 要求 5 "对于两个不同的参数k1和k2不相等,std::hash()(k1) == std::hash()(k2)的概率应该很小,接近1.0/std:: numeric_limits<:size_t>::max()。”这是我看到实现时一直怀疑的。当我看到这种特殊的电枢哈希函数的使用范围如此广泛时,我惊掉了下巴。 在这里对哈希组合进行了第一次研究,因为它非常有用。我的知识并不出色。所以如果我错了,请直说。上面的测试有一些奇怪的事情。 1) Wmath 命名空间实际上并未使用 2) 在模板中,在无符号类型上使用一元减号被认为是 VS 中的错误。 3) std::numeric_limits::digits-1 对于 uint32 的计算结果为 30,这是你想要的吗? 4) const T m = (std::numeric_limits::digits-1) 可以是 constexpr 在 VS 中使用内在函数 _rotl(8、16、32 或 64)专门化 rol() 怎么样?这些适用于所有常见尺寸。 “然后一个简单的异或就足够了” - 这似乎与后面的答案相矛盾,你注意到你应该做一些事情来确保你的 hash_combine 不会对 (x ,y) 和 (y,x)。可能值得编辑或更改它,以免读者错过一个非常重要的细微差别。 图看不懂,表看不懂。图像轴代表什么?颜色代表什么?这是两张图片还是一张矩形图片?关于表格:x 和 y 究竟代表什么?请让答案更白痴。【参考方案3】:

ROTL For VS studio(你可以轻松推导出 ROTR)。 (这实际上是对@WolfgangBrehm 的回复。)

原因:诱导编译器发出 ror 和/或 rol 指令的标准技巧在 VS 中给出错误:错误 C4146:一元减号运算符应用于无符号类型,结果仍然无符号。

所以...我通过将 (-c) 替换为 (T(0) -c) 解决了编译器错误,但这不会被优化。

添加(特定于 MS 的)专业化解决了这个问题,正如对已发出的优化程序集的检查所显示的那样。

#include <intrin.h>              // and some more includes, see above...

template <typename T>            // default template is not good for optimisation
typename std::enable_if<std::is_unsigned<T>::value, T>::type
constexpr rotl(const T n, const int i)

    constexpr T m = (std::numeric_limits<T>::digits - 1);
    const T c = i & m;
    //return (n << c) | (n >> (-c) & m);
    return (n << c) | (n >> (T(0) - c) & m);

template<>
inline uint32_t rotl(const uint32_t n, const int i)

    constexpr int m = (std::numeric_limits<uint32_t>::digits - 1);
    const int c = i & m;
    return _rotl(n, c);

template<>
inline uchar rotl(const uchar n, const int i)

    constexpr uchar m = (std::numeric_limits<uchar>::digits - 1);
    const uchar c = i & m;
    return _rotl8(n, c);

template<>
inline ushort rotl(const ushort n, const int i)

    constexpr uchar m = (std::numeric_limits<ushort>::digits - 1);
    const uchar c = i & m;
    return _rotl16(n, c);

template<>
inline uint64_t rotl(const uint64_t n, const int i)

    constexpr int m = (std::numeric_limits<uint64_t>::digits - 1);
    const int c = i & m;
    return _rotl64(n, c);

【讨论】:

我不认为这是***应该如何工作我担心答案需要是一个完整的答案。 (T(0) -c) 正是应该做的,然而,它仍然是 recognised by clang gccicc,但确实似乎没有被 msvc 识别,但我们正在谈论保存一个或两个易于流水线化的微指令,我不会担心。 reference suggestion 是 being recognised,所以你可能想用它来代替。

以上是关于C++ - 为啥 boost::hash_combine 是组合散列值的最佳方式?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 C++ 语法如此复杂? [关闭]

为啥 C++ 使用指针? [关闭]

用 c++ 输出,为啥?

为啥原生 C++ 项目有 TargetFrameworkVersion?

为啥 charles 无法通过 c++ 捕获 http 请求?

为啥要在 C++ 中按值传递对象 [重复]