我已经在其他帖子中读到,这似乎是组合哈希值的最佳方法。能否有人将其分解并解释为什么这是最佳方法?
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);
}
编辑:另一个问题只询问魔数,但我想了解整个函数,而不仅仅是这一部分。
我已经在其他帖子中读到,这似乎是组合哈希值的最佳方法。能否有人将其分解并解释为什么这是最佳方法?
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);
}
编辑:另一个问题只询问魔数,但我想了解整个函数,而不仅仅是这一部分。
说它是“最好的”是有争议的。
说它“好”,甚至“非常好”,至少在表面上很容易接受。
seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
我们假设seed
是hasher
或此算法的先前结果。
^=
意味着左侧和右侧的位都会更改结果的位。
hasher(v)
被认为是对v
的一种体面的哈希。但其余部分是为了防止它不是一个体面的哈希。
0x9e3779b9
是一个32位值(如果size_t
是64位,则可以将其扩展为64位), 它包含一半0和一半1。基本上,它是通过近似特定无理常数作为二进制小数点值来生成的随机0和1序列。这有助于确保如果哈希返回错误的值,我们仍然可以在输出中获得1和0的扩散。
(seed<<6) + (seed>>2)
是传入seed的位重排。0x
常数。假设哈希器几乎每个传入的v
都返回常量0x01000
。现在,种子的每个位都分布在下一次迭代的哈希中,在此期间它再次分布。
seed ^= (seed<<6) + (seed>>2)
0x00001000
经过一次迭代后变为0x00041400
,然后是0x00859500
。随着您重复此操作,任何集合位都会在输出位上“扩散”。最终,右侧和左侧的位会碰撞,并且Carry会将集合位从“偶数位置”移动到“奇数位置”。0x
常数添加了一堆伪随机位,使得无聊的哈希值被组合后占据了超过几个位的哈希空间。"dog"
和"god"
的哈希组合给出不同的结果),它处理无聊的哈希值(将字符映射为其ascii值,这仅涉及微调少量位)。并且,它速度相当快。seed -> (seed<<6) + (seed>>2)
是双射的? - Martin Rhash_combine(x,0)
,存在1346300007个冲突。 - Wolfgang Brehm这并不是最好的,让我惊讶的是它甚至不是特别好。主要问题在于分布不均,这不完全是boost::hash_combine
本身的错,而是与像std::hash
这样分布不良的哈希函数一起使用时出现的问题,而std::hash
通常实现为恒等函数。
图2:在一个随机的32位数字中改变单个位的效果对boost::hash_combine的结果的影响。x轴是输入位(两次32位,首先是新哈希值,然后是旧种子),y轴是输出位。颜色表示依赖程度。
为了证明事情会变得多么糟糕,以下是在使用hash_combine
时,对于32x32网格上的点(x,y)的碰撞情况,和使用std::hash
的情况:
# 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
#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));
}
在将种子组合成计算哈希的顺序之前,需要将其旋转一次,以使计算哈希的顺序变得相关。
boost
的 hash_combine
需要少两个操作,并且更重要的是没有乘法,实际上它快了约 5 倍,但在我的机器上每个哈希大约需要 2 个周期,所以建议的解决方案仍然非常快,而且在使用哈希表时很快就能收回成本。在 1024x1024 网格上有 118 个碰撞(与 boosts
的 hash_combine
+ std::hash
相比的 982017 个),与预期的良好分布的哈希函数数量相同,这是我们所能要求的。
即使与良好的哈希函数一起使用,boost::hash_combine
也不是理想的选择。如果所有熵都在种子中,那么其中一些将会丢失。对于 boost::hash_combine(x,0)
,存在 2948667289 种不同的结果,但应该有 4294967296 种。
总之,他们试图创建一个同时进行组合和级联的哈希函数,并且速度快,但最终得到的结果只是足够好,以至于不会立即被识别为不好。但它确实很快。
hash
函数已经定义,但在所呈现的源代码中没有使用。它使用了 std::hash
。为什么? - plasmacelROTL适用于VS Studio(您可以轻松推导ROTR)。 (实际上,这是回复@WolfgangBrehm的内容。)
原因:诱使编译器发出ror和/或rol指令的标准技巧在VS中会出现错误:error 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)
正是应该做的,然而,它仍然被 clang
、gcc
和 icc
所识别,但似乎确实没有被 msvc
所识别,但我们谈论的是节省一两个易于流水线化的微指令,我不会担心它。 - Wolfgang Brehm
boost::hash_combine
,而采用http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html。 - Maxim Egorushkin