接受整数散列键的整数散列函数是好的?

Posted

技术标签:

【中文标题】接受整数散列键的整数散列函数是好的?【英文标题】:What integer hash function are good that accepts an integer hash key? 【发布时间】:2010-10-14 10:07:59 【问题描述】:

【问题讨论】:

另见***.com/questions/12717413/… 【参考方案1】:

我发现以下算法提供了非常好的统计分布。每个输入位以大约 50% 的概率影响每个输出位。没有冲突(每个输入导致不同的输出)。该算法速度很快,除非 CPU 没有内置整数乘法单元。 C 代码,假设 int 是 32 位(对于 Java,将 >> 替换为 >>> 并删除 unsigned):

unsigned int hash(unsigned int x) 
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = (x >> 16) ^ x;
    return x;

使用运行了多个小时的special multi-threaded test program 计算了幻数,它计算了雪崩效应(如果单个输入位发生变化,输出位的数量会发生变化;平均应该接近 16),独立于输出位变化(输出位不应相互依赖),以及任何输入位发生变化时每个输出位发生变化的概率。计算的值优于MurmurHash 使用的 32 位终结器,并且几乎与使用 AES 时一样好(不完全)。一个轻微的优势是相同的常量被使用了两次(它确实使它在我上次测试时稍微快了一点,不确定是否仍然如此)。

如果将0x45d9f3b 替换为0x119de1f3(multiplicative inverse),则可以反转该过程(从哈希中获取输入值):

unsigned int unhash(unsigned int x) 
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = (x >> 16) ^ x;
    return x;

对于 64 位数字,我建议使用以下,即使它可能不是最快的。这个是基于splitmix64的,貌似是基于博客文章Better Bit Mixing(混13)。

uint64_t hash(uint64_t x) 
    x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
    x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
    x = x ^ (x >> 31);
    return x;

对于 Java,使用 long,将 L 添加到常量中,将 >> 替换为 >>> 并删除 unsigned。在这种情况下,倒车就更复杂了:

uint64_t unhash(uint64_t x) 
    x = (x ^ (x >> 31) ^ (x >> 62)) * UINT64_C(0x319642b2d24d8ec3);
    x = (x ^ (x >> 27) ^ (x >> 54)) * UINT64_C(0x96de1b173f119089);
    x = x ^ (x >> 30) ^ (x >> 60);
    return x;

更新:您可能还想查看Hash Function Prospector 项目,其中列出了其他(可能更好的)常量。

【讨论】:

前两行完全一样!这里有错字吗? 不,这不是错字,第二行进一步混合了这些位。只使用一个乘法并不好。 我更改了幻数,因为根据test case I wrote,值 0x45d9f3b 提供了更好的confusion and diffusion,特别是如果一个输出位发生变化,则其他输出位的变化概率大致相同(此外如果输入位发生变化,则所有输出位都以相同的概率发生变化)。您如何测量 0x3335b369 对您更有效? int 32 位适合你吗? 我正在为 64 位无符号整数到 32 位无符号整数寻找一个不错的散列函数。在那种情况下,上面的幻数会相同吗?我移动了 32 位而不是 16 位。 我相信在这种情况下,更大的因素会更好,但您需要运行一些测试。或者(这就是我所做的)首先使用x = ((x >> 32) ^ x),然后使用上面的 32 位乘法。我不确定什么更好。您可能还想看看the 64-bit finalizer for Murmur3【参考方案2】:

Knuth 的乘法方法:

hash(i)=i*2654435761 mod 2^32

一般来说,您应该选择一个与您的哈希大小(示例中为2^32)并没有公因数的乘数。这样哈希函数就可以统一覆盖所有的哈希空间。

编辑:这个散列函数的最大缺点是它保留了可分性,所以如果你的整数都可以被 2 或 4 整除(这并不罕见),它们的散列值也可以。这是哈希表中的一个问题 - 您最终只能使用 1/2 或 1/4 的存储桶。

【讨论】:

这是一个非常糟糕的哈希函数,尽管它附加了一个著名的名字。 如果与主要表大小一起使用,这根本不是一个糟糕的哈希函数。此外,它适用于 closed 散列。如果散列值不是均匀分布的,乘法散列可确保来自一个值的冲突不太可能“干扰”与其他散列值的项目。 为了好奇,这个常数被选为散列大小 (2^32) 除以 Phi Paolo:Knuth 的方法是“坏的”,因为它不会在高位发生雪崩 仔细观察后发现,2654435761 实际上是一个素数。所以这可能就是为什么选择它而不是 2654435769。【参考方案3】:

取决于您的数据的分布方式。对于一个简单的计数器,最简单的函数

f(i) = i

会很好(我怀疑是最优的,但我无法证明)。

【讨论】:

这样的问题是通常有大量整数可以被一个公因子整除(字对齐的内存地址等)。现在,如果您的哈希表恰好可以被相同的因子整除,那么您最终只使用了一半(或 1/4、1/8 等)的存储桶。 @Rafal:这就是为什么回复说“对于一个简单的计数器”和“取决于你的数据是如何分布的” 这其实是Sun对java.lang.Integer中hashCode()方法的实现grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… @JuandeCarrion 这具有误导性,因为这不是正在使用的哈希值。在转向使用两种表大小的幂之后,Java 重新散列从.hashCode() 返回的每个散列,请参阅here。 在许多实际应用中,身份函数作为散列是相当无用的,因为它具有分配属性(或缺乏),当然,除非局部性是所需的属性【参考方案4】:

快速且良好的哈希函数可以由质量较差的快速排列组合而成,例如

与一个不均匀的整数相乘 二进制旋转 xorshift

产生具有卓越品质的散列函数,例如用PCG 演示的随机数生成。

这实际上也是 rrxmrrxmsx_0 和 murmur hash 有意或无意地使用的配方。

我个人发现

uint64_t xorshift(const uint64_t& n,int i)
  return n^(n>>i);

uint64_t hash(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);

足够好。

一个好的哈希函数应该

    如果可能,双射不要丢失信息,并且冲突最少 尽可能多且均匀地级联,即每个输入位应以 0.5 的概率翻转每个输出位。

我们先来看看恒等函数。它满足 1. 但不满足 2. :

输入位 n 确定输出位 n 的相关性为 100%(红色),没有其他相关性,因此它们是蓝色的,给出了一条完美的红线。

xorshift(n,32) 也好不了多少,只产生一行半。仍然满足 1.,因为它可以通过第二个应用程序反转。

与无符号整数 ("Knuth's multiplicative method") 的乘法要好得多,级联更强烈,并且以 0.5 的概率翻转更多输出位,这就是你想要的,用绿色表示。满足1。对于每个不均匀的整数都有一个乘法逆元。

将两者结合得到以下输出,仍然满足 1. 因为两个双射函数的组合产生另一个双射函数。

乘法和 xorshift 的第二次应用将产生以下结果:

或者您可以使用像 GHash 这样的伽罗瓦域乘法,它们在现代 CPU 上变得相当快,并且一步就具有卓越的品质。

   uint64_t const inline gfmul(const uint64_t& i,const uint64_t& j)           
     __m128i I;I[0]^=i;                                                          
     __m128i J;J[0]^=j;                                                          
     __m128i M;M[0]^=0xb000000000000000ull;                                      
     __m128i X = _mm_clmulepi64_si128(I,J,0);                                      
     __m128i A = _mm_clmulepi64_si128(X,M,0);                                      
     __m128i B = _mm_clmulepi64_si128(A,M,0);                                      
     return A[0]^A[1]^B[1]^X[0]^X[1];                                              
   

【讨论】:

gfmul:代码似乎是伪代码,因为 afaik 不能在 __m128i 中使用括号。还是很有趣的。第一行似乎是说“取一个未初始化的 __m128i (I) 并与 (参数) i 进行异或。我应该将其读为用 0 初始化 I 并与 i 异或吗?如果是这样,它是否与使用 i 加载 I 相同?并对 I 执行非(操作)? @Jan 我想做的是__m128i I = i; //set the lower 64 bits,但我做不到,所以我使用^=0^1 = 1 因此不涉及。关于使用 进行初始化,我的编译器从未抱怨过,它可能不是最好的解决方案,但我想要的是将所有这些初始化为 0,这样我就可以做到 ^=|= 。我想我基于this blogpost 的代码,它也给出了反转,非常有用:D【参考方案5】:

32 位乘法方法(非常快)见 @rafal

#define hash32(x) ((x)*2654435761)
#define H_BITS 24 // Hashtable size
#define H_SHIFT (32-H_BITS)
unsigned hashtab[1<<H_BITS]  
.... 
unsigned slot = hash32(x) >> H_SHIFT

32 位和 64 位(良好分布)在:MurmurHash

Integer Hash Function

【讨论】:

【参考方案6】:

This page 列出了一些简单的散列函数,这些函数总体上趋于正常,但是任何简单的散列都有病态的情况,它不能很好地工作。

【讨论】:

【参考方案7】:

Eternally Confuzzled 对一些哈希算法进行了很好的概述。我推荐 Bob Jenkins 的一次一个哈希,它很快就会达到雪崩,因此可以用于高效的哈希表查找。

【讨论】:

这是一篇好文章,但它侧重于散列字符串键,而不是整数。 为了清楚起见,虽然本文中的方法适用于整数(或可以适应),但我假设有更有效的整数算法。【参考方案8】:

答案取决于很多事情,例如:

您打算在哪里使用它? 你想用哈希做什么? 您需要加密安全的哈希函数吗?

我建议您看看Merkle-Damgard 哈希函数系列,例如 SHA-1 等

【讨论】:

【参考方案9】:

自从我找到这个帖子以来,我一直在使用 splitmix64(指向 Thomas Mueller 的 answer)。然而,我最近偶然发现了 Pelle Evensen 的 rrxmrrxmsx_0,它产生了比最初的 MurmurHash3 终结器及其后继者(splitmix64 和其他混合)更好的统计分布。这是C语言中的sn-p代码:

#include <stdint.h>

static inline uint64_t ror64(uint64_t v, int r) 
    return (v >> r) | (v << (64 - r));


uint64_t rrxmrrxmsx_0(uint64_t v) 
    v ^= ror64(v, 25) ^ ror64(v, 50);
    v *= 0xA24BAED4963EE407UL;
    v ^= ror64(v, 24) ^ ror64(v, 49);
    v *= 0x9FB21C651E98DF25UL;
    return v ^ v >> 28;

Pelle 还提供in-depth analysis 的MurmurHash3 的最后一步中使用的 64 位混音器以及更新的变体。

【讨论】:

这个函数不是双射的。对于 v = ror(v,25) 的所有 v,即全 0 和全 1,它将在两个位置产生相同的输出。对于所有值 v = ror64(v, 24) ^ ror64(v, 49),它们至少还有两个,并且与 v = ror(v,28) 相同,产生另一个 2^4 ,总共大约 22 次不必要的碰撞. splitmix 的两个应用程序可能同样好和同样快,但仍然可逆且无碰撞。【参考方案10】:

我认为我们不能在事先不知道您的数据的情况下说散列函数是“好”的!并且不知道你将如何处理它。

对于未知数据大小,有比散列表更好的数据结构(我假设您在这里为散列表进行散列)。当我知道我有“有限”数量的元素需要存储在有限的内存中时,我会亲自使用哈希表。在我开始考虑我的哈希函数之前,我会尝试对我的数据进行快速统计分析,看看它是如何分布的等。

【讨论】:

【参考方案11】:

对于随机hash值,有工程师说黄金比例素数(2654435761)是一个不好的选择,我的测试结果发现不是真的;相反,2654435761 可以很好地分配哈希值。

#define MCR_HashTableSize 2^10

unsigned int
Hash_UInt_GRPrimeNumber(unsigned int key)

  key = key*2654435761 & (MCR_HashTableSize - 1)
  return key;

哈希表大小必须是 2 的幂。

我编写了一个测试程序来评估整数的许多哈希函数,结果表明 GRPrimeNumber 是一个不错的选择。

我试过了:

    total_data_entry_number / total_bucket_number = 2、3、4;其中 total_bucket_number = 哈希表大小; 将哈希值域映射到桶索引域;即用(hash_table_size - 1)进行逻辑与运算将hash值转化为桶索引,如Hash_UInt_GRPrimeNumber()所示; 计算每个桶的碰撞次数; 记录未被映射的bucket,即空bucket; 找出所有桶的最大碰撞次数;即最长的链长;

根据我的测试结果,我发现黄金比例素数总是有更少的空桶或零空桶和最短的碰撞链长度。

一些整数的hash函数号称是好的,但测试结果表明,当total_data_entry / total_bucket_number = 3时,最长链长度大于10(最大碰撞数> 10),很多桶没有映射(空桶),与黄金比例素数散列的零空桶和最长链长度3的结果相比,这是非常糟糕的。

顺便说一句,根据我的测试结果,我发现一个版本的移位异或哈希函数非常好(由 mikera 共享)。

unsigned int Hash_UInt_M3(unsigned int key)

  key ^= (key << 13);
  key ^= (key >> 17);    
  key ^= (key << 5); 
  return key;

【讨论】:

但是为什么不把产品右移,这样你就可以保留最混合的位呢?这就是它应该工作的方式 @harold,黄金比例素数是经过精心挑选的,虽然我认为它不会有任何区别,但我会测试一下它是否与“最混合的位”更好。虽然我的观点是“这不是一个好的选择”。不正确,测试结果表明,只抓取下半部分的bits就足够了,甚至比很多hash函数都好。 (2654435761, 4295203489) 是素数的黄金比例。 (1640565991, 2654435761) 也是质数的黄金比例。 @harold,产品右移变差了,即使只是右移1位(除以2),还是变差了(虽然还是0个空桶,但是最长链长更大) ;右移更多的位置,结果变得更糟。为什么?我认为原因是:将产品向右移动会使更多的哈希值不互质,只是我的猜测,真正的原因涉及数论。

以上是关于接受整数散列键的整数散列函数是好的?的主要内容,如果未能解决你的问题,请参考以下文章

Redis数据操作之字符串与散列键的区别 | Redis

Redis数据操作--散列键

根据散列键名批量删除redis散列值

Redis数据操作之散列键 | Redis

对于 unordered_map,对于具有 3 个无符号字符和一个 int 的结构,啥是好的散列函数?

Perl - 使用字符串代替散列键