将32位整数哈希为16位整数?

19

有什么简单的方法可以将一个32位整数(例如IP地址,例如Unix time_t等)哈希成一个16位整数?

例如,hash_32b_to_16b(0x12345678) 可能会返回 0xABCD

让我们从这个可怕但实用的例子解决方案开始:

function hash_32b_to_16b(val32b) {
    return val32b % 0xffff;
}

这个问题特别涉及到JavaScript,但是欢迎添加任何与语言无关的解决方案,最好不要使用库函数。

此问题的背景是生成唯一的ID(例如,64位ID可能由各种32位值的几个16位哈希组成)。避免碰撞很重要。

简单=好。疯狂+混淆=有趣。


1
将高2字节与低2字节进行异或?0x1234 XOR 0x5678。但您不能标记问题为“密码学”,并要求类似的内容... - Remus Rusanu
对于Remus的观点,我同意这与密码学无关。如果我理解正确,你的16位哈希将映射到两个32位整数中的一个。我很好奇你试图解决的具体问题,并且希望它与安全无关。 - John Bledsoe
1
与前面的评论类似,由于无法用16位数表示与32位数相同数量的唯一性,因此您可能只需取一半数字。例如,0x1234或0x5678。通过这种方式,至少唯一性的损失对于未来代码维护者来说应该是非常明显的。 - lzcd
1
加密是哈希函数中一种可能的“好”的类型。它意味着明文和哈希值之间有一定程度的分离。这里的第一个评论没有这种(加密)质量,但对于许多用途来说仍然是一个好的哈希函数。 - Slartibartfast
17
以下页面提供了几种通用哈希函数的实现,它们高效且碰撞最少:http://partow.net/programming/hashfunctions/index.html。 - Matthieu N.
显示剩余6条评论
6个回答

10
最大限度保留某个原始32位“信号”的熵的关键在于确保每个32个输入位都具有独立且相等的能力来改变16位输出字的值。由于OP请求的位大小恰好是原始位的一半,因此满足此标准的最简单方法是对上半部分和下半部分进行异或操作,如其他人所提到的那样。使用异或操作是最优的,因为根据异或操作的定义,独立翻转任何一个32位输入位都保证会改变16位输出的值,这是显而易见的。
当你需要将输入从32位减少到2位时,问题变得更加有趣。记住,目标是尽可能保留源中的熵,因此那些简单地使用(i & 3)掩码处理最低的两个位的解决方案通常是错误的方向;这样做保证了除未掩码位之外的任何位都不会影响结果,这通常意味着运行时信号的任意可能有价值的部分被无原则地丢弃。从前面的段落可以得出,你当然可以使用xor进行三次迭代以产生具有所需属性的2位输出,即每个/任何输入位均受到同等影响。当然,这种解决方案仍然是最优的正确方案,但涉及循环或多个展开操作,而这些并不是必要的!

幸运的是,有一种只需要两个操作的好技巧,可以在这种情况下得到相同的最优结果。与xor一样,它不仅确保对于任何给定的32位值,扭曲任何输入位都将导致2位输出的更改,而且还确保在给定输入值的均匀分布的情况下,2位输出值的分布也将完全均匀。在当前示例中,该方法将4,294,967,296个可能的输入值分成恰好1,073,741,824个四个可能的2位哈希结果{ 0, 1, 2, 3 }

我在这里提到的方法使用了我通过详尽搜索发现的特定魔法值,这些值似乎在互联网上没有被讨论得很多,至少对于此处讨论的特定用途(即确保最大熵保持均匀哈希分布)。奇怪的是,根据同样详尽的搜索,这些魔法值实际上是唯一的,这意味着对于每个目标位宽{16、8、4、2},我下面展示的魔法值是唯一的值,当按照我在这里展示的方式使用时,满足上述完美哈希条件。

不再拖延,将32位哈希为n = {16、8、4、2}的唯一且数学上最优过程是乘以n相对应的魔法值(无符号,舍弃溢出),然后取结果的n高位。要将这些结果位隔离为[0 ... (2ⁿ - 1)]范围内的哈希值,只需将乘法结果向右移动32 - n位(无符号!)。

“神奇”的值和类C表达式语法如下:

方法

将32位减少到最大熵保留哈希. . .

目标位数    乘数    右移位数       表达式 [1, 2]
-----------   ------------   -----------   -----------------------
    16         0x80008001        16        (i * 0x80008001) >> 16
     8         0x80808081        24        (i * 0x80808081) >> 24
     4         0x88888889        28        (i * 0x88888889) >> 28
     2         0xAAAAAAAB        30        (i * 0xAAAAAAAB) >> 30

将64位减少到最大熵保留哈希. . .

目标位数   乘数                右移位数     表达式 [1, 2]
-----------   ------------------   -----------   -------------------------------
    32        0x8000000080000001       32        (i * 0x8000000080000001) >> 32
    16        0x8000800080008001       48        (i * 0x8000800080008001) >> 48
     8        0x8080808080808081       56        (i * 0x8080808080808081) >> 56
     4        0x8888888888888889       60        (i * 0x8888888888888889) >> 60
     2        0xAAAAAAAAAAAAAAAB       62        (i * 0xAAAAAAAAAAAAAAAB) >> 62

注释:

  1. 使用无符号乘法,并丢弃任何溢出(不需要64位乘法)。
  2. 如果使用右移位来隔离结果(如图所示),请务必使用无符号移位操作。

进一步讨论

我觉得这很酷。在实际应用中,关键的信息理论要求是保证对于任何m位输入值及其对应的n位哈希值结果,翻转任何一个m源位总是会导致n位结果值发生变化。尽管总共有2ⁿ种可能的结果值,但其中一种已经“被使用”(由结果本身),因为从任何其他结果“切换”到该结果将不会有任何变化。这留下了2ⁿ - 1个结果值,可供整个由单个位翻转的m输入值组成的集合使用。

让我们来考虑一个例子;事实上,为了展示这种技术可能看起来有点神秘或者非常神奇,我们将考虑更极端的情况,其中m = 64n = 2。使用2个输出比特位,有四种可能的结果值,分别是{0, 1, 2, 3}。假设一个任意的64位输入值0x7521d9318fbdf523,我们会得到它的2位哈希值1

 (0x7521d9318fbdf523 * 0xAAAAAAAAAAAAAAAB) >> 62   // result -->  '1'

因此,结果为1,而声明是在单个位0x7521d9318fbdf523被切换的64个值集合中,没有任何一个值可能具有相同的结果值。也就是说,这64个其他结果中没有一个可以使用值1,而必须使用023。因此,在这个例子中,似乎每一个2⁶⁴个输入值——除了另外64个输入值——都会自私地独占输出空间的四分之一。考虑到这些交互约束的巨大数量,是否存在一个同时满足的解决方案?
嗯,当然,为了证明确实存在(精确地说),以下是哈希结果值的列表,按顺序列出了从最高位(位置63)向最低位(0)逐个翻转0x7521d9318fbdf523的单个位的输入。
3 2 0 3 3 3 3 3 3 0 0 0 3 0 3 3 0 3 3 3 0 0 3 3 3 0 0 3 3 0 3 3  // continued…
0 0 3 0 0 3 0 3 0 0 0 3 0 3 3 3 0 3 0 3 3 3 3 3 3 0 0 0 3 0 0 3  // notice: no '1' values

正如您所见,没有 1 值,这意味着源代码中的每一位都必须作出贡献才能影响结果(或者,如果你喜欢,0x7521d9318fbdf523 中每一位的实际状态对于防止整个结果变为“非 1”是至关重要的)。因为无论您对 64 位输入进行何种单比特更改,2 比特结果值都将不再是 1
请记住,上面显示的“缺失值”表格仅从分析一个随机选择的示例值 0x7521d9318fbdf523 中转储;每个其他可能的输入值都有自己的类似表格,每个表格都神秘地缺少其所有者的实际结果值,但在其集合成员中却以某种方式保持全局一致。这种属性基本上对应于在(固有的有损)位宽缩减任务期间最大程度地保留可用熵。
因此,我们可以看到每个可能的源值中的每一个独立地强加了在恰好 64 个其他源值上排除可能结果值的约束。使我感到困惑的是,有无数个这些 64 成员集合,每个成员也属于其他 63 个看似不相关的位操作集合。然而,尽管存在这种最令人费解的交织约束难题,却很容易利用其中一种(我猜测)解决方案,同时完全满足它们所有。
所有这些似乎都与您在上面的表格中注意到的某些内容有关:即,我没有看到任何明显的方法将技术扩展到压缩到 1 位结果的情况。在这种情况下,只有两个可能的结果值 {0,1},因此,如果任何/每个给定的(例如)64 位输入值仍然概括地排除其自身的结果成为其 64 个单位翻转邻居的所有结果之一,则这现在基本上会将另一个剩余值强加于这 64 个值上。我们在表格中看到的数学分解似乎在暗示这样的条件下的同时结果是一个太过遥远的目标。

换句话说,xor 的特殊'信息保留'特性(也就是它的高度可靠的保证,与andor等不同,它总是可以并且将会改变一位)自然会有一定的代价,即强烈的非协商要求一定的操作空间——至少2个比特。


6

我认为这是你能得到的最好结果。你可以将代码压缩成一行,但现在变量还在那里作为文档:

function hash_32b_to_16b(val32b) {
    var rightBits = val32b & 0xffff; // Left-most 16 bits
    var leftBits = val32b & 0xffff0000; // Right-most 16 bits

    leftBits = leftBits >>> 16; // Shift the left-most 16 bits to a 16-bit value

    return rightBits ^ leftBits; // XOR the left-most and right-most bits
}

考虑到问题的参数,最佳解决方案应该让每个16位哈希值对应恰好2^16个32位数字。我认为它还应该以不同的方式哈希连续的32位数字。除非我遗漏了什么,否则我相信这个解决方案做到了这两点。

我认为在这个问题中安全性不能成为考虑因素,因为哈希值只有太少的位数。我相信我提供的解决方案可以将32位数字均匀地分配给16位哈希值。


你为什么认为这是最好的呢?我认为它可以在有用和频繁的数字中获得大量的碰撞。 - Rotsor
3
这不是最好的想法。原因是IP地址通常被分配为连续的子网。这意味着,如果IP地址A.B.C.D存在于一个网络中,那么A.(B^1).C.D和A.B.C.(D^1)也更有可能存在,并且将获得相同的哈希值。显然,任何哈希函数都会有很多碰撞。但是您的方案将比从均匀分布的32位整数挑选并进行哈希处理时预期的碰撞更多。通过稍微混淆一下位,您将获得更好的结果。 - sigfpe
1
你用来评估哈希函数质量的标准,即使是对于更简单的哈希函数 hash = val&0xffff 也适用。然而,这些函数在实际数据上发生冲突的概率是不同的。 - Rotsor
@Rostor Ha,您说得对。所有这些中的百万美元问题是所涉及数据的分布情况。 - John Bledsoe

3
这取决于整数的性质。如果它们可以包含一些位掩码,或者可以通过2的幂次差异,则简单的XOR运算将具有高碰撞概率。您可以尝试使用(i>>16) ^ ((i&0xffff) * p),其中p是一个质数。
像MD5这样的安全哈希都很好,但在这里显然过于复杂。任何比CRC16更复杂的东西都是过度设计。

这是一个有趣的观点,显然与哈希IP地址相关,对吗? - dkamins
是的。对于时间值,i&0xffff通常足够了。(希望没有任何地方使用sleep(65536);) - Rotsor
2
除非您确切地知道输入数据,否则无法确定什么是“足够的”。最坏情况下的碰撞次数仍将保持不变。 乘以质数只会使找到产生系统性碰撞的真实情况更加困难。(您的delta-time是1009的倍数有多少次?) 为什么质数更好是一个长话题 - Rotsor
请注意返回的数字可能超过16位。你可以使用这种方式进行操作:((i>>16) ^ ((i&0xffff) * p) & 0xffff)(但我不是专家)。 - clankill3r

2
我建议使用标准哈希算法,如sha1或md5,然后获取其最后16位。

SHA1或MD5对于短输入流(如4个字节)可能会有问题吗? - dkamins
在JavaScript环境中通常不提供sh1和md5。是否有一些稍微不那么安全但极其简化的版本可以用几行JS表达? - dkamins

2
假设您希望最不重要的比特位变化最大,我认为仅使用值的低16位作为哈希值就足够了,可以得到足够好的分布。
如果您将要哈希的数字没有那种分布,则异或上32位可能会有所帮助。
当然,这样的建议仅适用于您打算将哈希用于某种查找/存储方案,并且不寻求加密相关的非猜测性和不可逆性属性(异或建议也无法带来这些属性)。

0

像这样简单的东西...

function hash_32b_to_16b(val32b) {    
    var h = hmac(secretKey, sha512);
    var v = val32b;
    for(var i = 0; i < 4096; ++i)
        v = h(v);
    return v % 0xffff;
}

2
为了减慢速度。 这是哈希密码的常见技术,使得创建彩虹表或暴力破解密码变得更加困难数个数量级。 - yfeldblum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接