为什么XOR是组合哈希的默认方式?

192
假设你有两个哈希值H(A)H(B),你想将它们组合起来。 我读到过一种好的方法是对它们进行XOR运算,例如XOR( H(A), H(B) )

我找到的最好的解释在这里:hash function guidelines中简短地提及:
使用具有大致随机分布的两个数字进行XOR运算将产生另一个仍具有大致随机分布的数字*,但现在依赖于两个值。
...
* 在要合并的两个数字的每个二进制位上,如果两个比特相等,则输出0,否则输出1。换句话说,在50%的组合中,将输出1。因此,如果两个输入比特各自具有大约50-50的成为0或1的机会,则输出比特也是如此。
可以解释一下为什么XOR应该是默认用于组合哈希函数(而不是OR或AND等)的运算符背后的直觉和/或数学吗?

25
我想你刚刚已经做到了 ;) - Massa
25
注意,根据您对“组合”中所需的内容,异或(XOR)可能还不是一种“好”的方式。异或是可交换的:XOR(H(A),H(B))等于XOR(H(B),H(A))。这意味着XOR不是创建有序值序列的哈希类型的正确方法,因为它不能捕获顺序。 - Thomas Pornin
6
除了上面的评论中提到的排序问题外,等值问题也存在。对于任何函数H,XOR(H(1), H(1))=0,XOR(H(2),H(2))=0等等。对于任何N:XOR(H(N),H(N))=0。在真实应用程序中,等值情况经常发生,这意味着XOR的结果会过于频繁地为0,无法被视为好的哈希。 - Andrei Galatyn
你用什么来表示有序的值序列?比如说,我想创建一个时间戳或索引的哈希表(最高位比最低位不重要)。如果这个帖子已经过了一年,请原谅。 - Alexis
一个警告:不要使用XOR来组合CRC值,因为CRC是线性函数,即CRC(a) ^ CRC(b) = CRC(a ^ b)。此外,两个相等的元素将被取消。如果您想要无序列表的哈希,则使用CRC值(通过加法)求和应该是可以的,但我对此并不100%确定。 - Dan Stahlke
9个回答

236

xor是一种在哈希时使用的危险默认函数。它比andor好,但这并不说太多。

xor是对称的,因此元素的顺序丢失了。所以"bad"将与"dab"结合哈希相同。

xor将成对相同的值映射为零,并且您应该避免将“常见”值映射为零:

因此,(a,a)被映射为0,(b,b)也被映射为0。由于此类对几乎总是比随机性暗示的更常见,因此在零处发生的碰撞比应有的要多得多。

由于这两个问题,xor最终成为一个看起来还不错但经过进一步检查会变得不行的哈希组合器。

在现代硬件上,加法通常与xor的速度差不多(诚然,它可能需要更多的功率来完成这项工作)。添加的真值表在相关位上与xor类似,但当两个值都为1时,它还会将一个位发送到下一个位。这意味着它擦除的信息更少。

因此,hash(a) + hash(b)hash(a) xor hash(b)更好,如果a==b,则结果为hash(a)<<1而不是0。

这仍然是对称的;因此,"bad""dab"得到相同的结果仍然是个问题。我们可以以适度的代价打破这个对称性:

hash(a)<<1 + hash(a) + hash(b)

也被称为 hash(a)*3 + hash(b)。(如果使用移位方案,则建议计算并存储一次 hash(a))。选择任意奇数常量代替 3,将“k 位”无符号整数双射到自身,因为无符号整数的映射在模数为某个k的数学下取模于2^k,而任何奇数常量都与2^k互质。

对于更高级的版本,我们可以查看 boost::hash_combine,它实际上是:

size_t hash_combine( size_t lhs, size_t rhs ) {
  lhs ^= rhs + 0x9e3779b9 + (lhs << 6) + (lhs >> 2);
  return lhs;
}

这里我们将一些偏移版本的lhs与常数相加(基本上是随机的01 - 特别地,它是32位定点小数的黄金比例倒数),并进行一些加法和异或操作。这样可以破坏对称性,并在输入的哈希值很差时引入一些“噪音”(例如,假设每个组件都哈希为0-上述方法可以很好地处理,每次组合后生成一堆10)。我的简单的3*hash(a)+hash(b)在这种情况下只会输出0)。

将此扩展到64位(使用pi的扩展作为我们的64位常数,因为它在64位时是奇数):

size_t hash_combine( size_t lhs, size_t rhs ) {
  if constexpr (sizeof(size_t) >= 8) {
    lhs ^= rhs + 0x517cc1b727220a95 + (lhs << 6) + (lhs >> 2);
  } else {
    lhs ^= rhs + 0x9e3779b9 + (lhs << 6) + (lhs >> 2);
  }
  return lhs;
}

(对于那些不熟悉C/C++的人来说,size_t是一个无符号整数值,它足以描述内存中任何对象的大小。在64位系统上,它通常是一个64位无符号整数。在32位系统上,它是一个32位无符号整数。)


很好的回答,Yakk。这个算法在32位和64位系统上同样有效吗?谢谢。 - Dave
1
@dave 给 0x9e3779b9 添加更多的位。 - Yakk - Adam Nevraumont
15
好的,为了完整起见... 这是一个完整的精度为64位的常量(使用长双精度浮点数和无符号长整型计算得出):0x9e3779b97f4a7c16。有趣的是,它仍然是偶数。重新使用圆周率而不是黄金分割进行相同的计算会得到一个奇数:0x517cc1b727220a95,因此可能比另一个常量更加“质数”。我使用了如下代码:std::cout << std::hex << (unsigned long long) ((1.0L/3.14159265358979323846264338327950288419716939937510L)*(powl(2.0L,64.0L))) << std::endl; 并使用cout.precision(numeric_limits<long double>::max_digits10);。再次感谢Yakk。 - Dave
3
对于这些情况,逆黄金比例规则是第一个_大于或等于计算结果的奇数。所以只需加1。这是一个重要的数字,因为N * 比率的序列,模最大值(这里是2^64)将下一个值精确地放置在最大“间隙”中点处的比率位置。搜索“斐波那契哈希”以获取更多信息。 - Scott Carey
1
@Dave 正确的数字应该是0.9E3779B97F4A7C15F39... 请参考链接。你可能正在遭受四舍六入五成双的规则(这对会计师来说很好),或者简单地说,如果你从一个字面上的sqrt(5)常量开始,当你减去1时,你会丢失高位比特,一个比特必须已经丢失了。 - migle
显示剩余14条评论

141

假设输入是均匀随机的(1位),AND函数的输出概率分布为75%的0和25%的1。相反,OR的输出概率分布为25%的0和75%的1

XOR函数的输出概率分布为50%的0和50%的1,因此它很适合用于组合均匀概率分布。

这可以通过列出真值表来证明:

 a | b | a AND b
---+---+--------
 0 | 0 |    0
 0 | 1 |    0
 1 | 0 |    0
 1 | 1 |    1

 a | b | a OR b
---+---+--------
 0 | 0 |    0
 0 | 1 |    1
 1 | 0 |    1
 1 | 1 |    1

 a | b | a XOR b
---+---+--------
 0 | 0 |    0
 0 | 1 |    1
 1 | 0 |    1
 1 | 1 |    0

练习:有多少个逻辑函数使用两个1比特输入ab具有这种均匀的输出分布?为什么异或是最适合你在问题中陈述的目的的函数?


27
回答练习题:从16种可能的不同a XXX b操作(0,a&b,a> b,a,a < b,b,a%b,a | b,! a&!b,a == b,!b,a> = b,!a,a <= b,!a |!b,1),以下假设a和b具有50%-50%的0和1分布情况下,具有50%-50%的0和1分布的a和b: a,b,!a,!b,a%b,a == b,即可以使用相反的XOR(EQUIV)... - Massa
9
Greg,这个答案太棒了。在看了你的原始答案并写出自己的真值表之后,我恍然大悟了。我考虑了 @Massa 的答案,关于有6个适合维持分布的操作。而且虽然 a、b、!a、!b 与它们各自的输入具有相同的分布,但你会失去另一个输入的熵。也就是说,异或运算最适合于合并哈希,因为我们想从 a 和 b 中捕捉熵。 - Nate Murray
3
@Massa,我从未见过在XOR或不等式中使用%。 - Buge
8
正如Yakk指出的,异或运算在产生相同值时可能会造成问题。这意味着(a,a)(b,b)都会产生零,这在基于哈希的数据结构中很多(甚至大多数)情况下会极大地增加碰撞的可能性。 - Drew Noakes
2
考虑对两个字节进行异或运算,有256*256种可能的输入值,但只有256种输出值。假设所有三个值都具有相同的选项,则不可能得出唯一的输出。 - Drew Noakes
显示剩余6条评论

33

尽管异或具有方便的位混合特性,但由于其交换律,它不是组合哈希的好方法。考虑一下如果您将{1, 2,…,10}的排列存储在一个10元组的哈希表中会发生什么。

一个更好的选择是m * H(A) + H(B),其中m是一个大奇数。

来源:上述组合器是Bob Jenkins的一个提示。


2
有时可逆律是一件好事,但即使在那种情况下,异或也是一个糟糕的选择,因为所有匹配项对都会被散列为零。算术和更好;一对匹配项的哈希将仅保留31位有用数据,而不是32位,但这比保留零好得多。另一个选项可能是将算术和计算为“long”,然后将上部分与下部分混合。 - supercat
1
m = 3 实际上是一个很好的选择,在许多系统上非常快。请注意,对于任何奇数 m 整数乘法都是模 2^322^64,因此可逆,因此您不会丢失任何位。 - StefanKarpinski
当您超出MaxInt时会发生什么? - disruptive
2
应该选择质数而不是奇数。 - TermoTux
2
@Infinum 在合并哈希时不需要这样做。 - Marcelo Cantos
显示剩余3条评论

18

Xor可能是组合哈希的“默认”方式,但Greg Hewgill的答案也说明了为什么它有其缺陷:

两个相同哈希值的异或结果是零。

在实际生活中,相同的哈希更常见,这可能比您想象的要多。您可能会发现,在这些(不那么罕见的)极端情况下,结果组合的哈希总是相同的(零)。哈希冲突将比您预期的频繁得多。

在一个刻意构造的例子中,您可能正在组合来自您管理的不同网站的用户的散列密码。不幸的是,很多用户重复使用他们的密码,结果哈希的比例令人惊讶地为零!


1
我希望这个编造的例子永远不会发生,密码应该加盐。 - flaviut

8

我想要明确指出一些内容,以帮助其他人理解这个页面。AND和OR会像BlueRaja - Danny Pflughoe所指出的那样限制输出结果,但可以更好地定义:

首先,我想定义两个简单的函数来解释这个问题:Min()和Max()。

Min(A, B)将返回A和B之间较小的值,例如:Min(1, 5)返回1。

Max(A, B)将返回A和B之间较大的值,例如:Max(1, 5)返回5。

如果给定:C = A AND B

那么你可以发现C <= Min(A, B)。我们知道这是因为没有任何东西可以与A或B的0位相与使它们变成1。因此,每个零位保持为零,每个一位都有机会变成零位(从而成为一个更小的值)。

对于:C = A OR B

相反的情况是成立的:C >= Max(A, B)。通过这个,我们可以看到AND函数的推论。任何已经是1的位不能被OR成0,因此它们保持为1,但每个零位都有机会变成1,从而成为一个更大的数。

这意味着输入的状态会限制输出。如果你将90与任何东西进行AND运算,你知道输出结果将等于或小于90,而不管其他值是什么。

对于XOR,没有基于输入的暗示限制。有特殊情况可以发现,如果你将一个字节与255进行XOR,则会得到相反的结果,但可能会输出任何可能的字节。每个位都有可能根据另一个操作数中的相同位改变状态。


7
可以说,OR是按位最大值,而AND是按位最小值。 - Paŭlo Ebermann
非常好的陈述,Paulo Ebermann。很高兴在这里见到你和Crypto.SE! - Corey Ogburn
我创建了一个过滤器(http://stackexchange.com/filters/19305/small-tags-sites),其中包括所有标记为[tag:cryptography]的内容,还包括旧问题的更改。通过这种方式,我在这里找到了你的答案。 - Paŭlo Ebermann

4
如果你将一个随机输入与一个有偏差的输入进行XOR运算,输出结果是随机的。但对于ANDOR运算则不成立。例如:
00101001 XOR 00000000 = 00101001
00101001 AND 00000000 = 00000000
00101001 OR  11111111 = 11111111
正如@Greg Hewgill所提到的,即使两个输入都是随机的,使用ANDOR也会导致偏差的输出。
我们之所以使用XOR而不是更复杂的方法,是因为没有必要: XOR可以完美地工作,并且速度非常快。

3

覆盖左侧的两列,仅使用输出结果来确定输入内容。

 a | b | a AND b
---+---+--------
 0 | 0 |    0
 0 | 1 |    0
 1 | 0 |    0
 1 | 1 |    1

当您看到1位时,应该意识到两个输入都为1。

现在对于XOR也做同样的事情。

 a | b | a XOR b
---+---+--------
 0 | 0 |    0
 0 | 1 |    1
 1 | 0 |    1
 1 | 1 |    0

XOR不会泄露任何关于其输入的信息。

1

XOR 不像 ORAND 有时会忽略部分输入。

AND(X, Y) 为例,如果将输入 X 设为 false,则输入 Y 就不重要了...而在组合哈希时,可能希望输入是重要的。

如果采用 XOR(X, Y),则两个输入始终都很重要。不存在 X 的某个值使得 Y 不重要。如果更改 X 或 Y 中的任何一个,则输出也会反映出来。


0

java.util.Arrays中各个版本hashCode()的源代码是一个出色的参考,适用于稳定、通用的哈希算法。它们容易被理解并转换到其他编程语言。

粗略地说,大多数多属性hashCode()实现都遵循这种模式:

public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

您可以搜索其他StackOverflow Q&A以获取有关31背后的魔法以及为什么Java代码经常使用它的更多信息。它并不完美,但具有非常好的通用性能特征。

2
Java的默认“乘以31并加/累加”哈希存在冲突(例如,任何string都与string +“AA”发生冲突,如果我没记错的话),而且他们很久以前就希望没有将该算法嵌入规范中。 也就是说,使用更大的奇数,并添加移位或旋转可以解决这个问题。 MurmurHash3的“mix”就是这样做的。 - Scott Carey

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