这不是最好的,令我惊讶的是它甚至不是特别好。主要问题是糟糕的分布,这并不是 boost::hash_combine 本身的错,而是与像 std::hash 这样的糟糕分布的散列一起使用,这种散列最常使用标识函数实现。
图 2:两个随机 32 位数字之一的单个位更改对 boost::hash_combine 结果的影响。 x 轴上是输入位(两次 32,首先是新哈希,然后是旧种子),y 轴上是输出位。颜色表示依赖程度。
为了证明事情会变得多么糟糕,当按预期使用hash_combine 和std::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));
}
种子在组合之前旋转一次,以使计算哈希的顺序相关。
来自boost 的hash_combine 需要更少的两个操作,更重要的是不需要乘法,实际上它快了大约 5 倍,但是在我的机器上每个哈希大约 2 cyles,所提出的解决方案仍然非常快并且很快就会得到回报当用于哈希表时。在 1024x1024 网格上有 118 次碰撞(boostshash_combine + std::hash 的碰撞次数为 982017),这与分布良好的哈希函数的预期一样多,这就是我们所能要求的。
现在即使与良好的散列函数 boost::hash_combine 结合使用也不理想。如果所有熵在某个时候都在种子中,其中一些会丢失。 boost::hash_combine(x,0) 有 2948667289 个不同的结果,但应该有 4294967296。
总之,他们试图创建一个哈希函数,它可以同时进行组合和级联,而且速度很快,但最终得到的结果是两者都做得很好,不会立即被认为是坏的。但速度很快。