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

Posted

技术标签:

【中文标题】为啥 XOR 是组合哈希的默认方式?【英文标题】:Why is XOR the default way to combine hashes?为什么 XOR 是组合哈希的默认方式? 【发布时间】:2011-08-18 20:32:02 【问题描述】:

假设您有两个哈希 H(A)H(B),并且您想将它们组合起来。我读过将两个散列组合在一起的好方法是XOR 他们,例如XOR( H(A), H(B) ).

我找到的最好的解释是在这些hash function guidelines 上简要介绍的:

对具有大致随机分布的两个数字进行异或运算会得到另一个仍然具有大致随机分布的数字*,但现在取决于这两个值。 ... * 在要组合的两个数字的每个位上,如果两个位相等,则输出 0,否则输出 1。换句话说,在 50% 的组合中,将输出 1。因此,如果两个输入位每个都有大约 50-50 的机会是 0 或 1,那么输出位也是如此。

您能否解释一下为什么 XOR 应该是组合哈希函数(而不是 OR 或 AND 等)的默认操作背后的直觉和/或数学?

【问题讨论】:

我想你刚刚做到了 ;) 请注意,XOR 可能是也可能不是“组合”哈希的“好”方式,具体取决于您在“组合”中想要的内容。 XOR 是可交换的:XOR(H(A),H(B)) 等于 XOR(H(B),H(A))。这意味着 XOR 不是创建一种有序值序列散列的正确方法,因为它不捕获顺序。 除了顺序的问题(上面的评论),还有相等值的问题。 XOR(H(1), H(1))=0(对于任何函数 H),XOR(H(2),H(2))=0 等等。对于任何 N:XOR(H(N),H(N))=0。相等的值在实际应用中经常发生,这意味着 XOR 的结果将经常为 0,而不能被视为良好的哈希。 你对有序的值序列使用什么?假设我想创建时间戳或索引的哈希。 (MSB 不如 LSB 重要)。对不起,如果这个线程是 1 岁。 相关:What is the best algorithm for an overridden System.Object.GetHashCode? 【参考方案1】:

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-bit”无符号整数到自身,因为无符号整数上的映射对于某些k 是数学模2^k,并且任何奇数常数都相对于@ 987654347@.

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

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

在这里,我们将lhs 的一些移位版本与一个常数相加(这基本上是随机的0s 和1s - 特别是作为 32 位定点分数的黄金比例的倒数)加上一些加法和异或。这破坏了对称性,并且如果传入的散列值很差(即,假设每个组件散列为 0 - 以上处理它很好,在每次合并后生成 10s 的污点)会引入一些“噪音”。我的在这种情况下,天真的 3*hash(a)+hash(b) 只会输出 0

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

【讨论】:

不错的答案 Yakk。该算法在 32 位和 64 位系统上是否同样适用?谢谢。 @dave 向0x9e3779b9 添加更多位。 好的,要完整...这里是全精度 64 位常量(使用 long double 和 unsigned long long 计算):0x9e3779b97f4a7c16。有趣的是,它仍然是偶数。使用 PI 而不是黄金比例重新进行相同的计算会产生: 0x517cc1b727220a95 这是奇数,而不是偶数,因此可能比另一个常数“更多”。我用过: std::cout ::max_digits10 );再次感谢 Yakk。 @Dave 这些情况的逆黄金比例规则是第一个 odd 数等于或大于您正在执行的计算。所以只需加 1。这是一个重要的数字,因为 N * 比率的序列,mod 最大大小(此处为 2^64)将序列中的下一个值恰好以该比率放置在最大“间隙”的中间数字。在网上搜索“斐波那契哈希”以获取更多信息。 @Dave 正确的数字是 0.9E3779B97F4A7C15F39... 见link。您可能会遇到四舍五入规则(这对会计师有好处),或者简单地说,如果您从字面量 sqrt(5) 常量开始,当您减去 1 时,您会删除高位,a一定是丢失了。【参考方案2】:

XOR 有时不会忽略某些输入,例如 ORAND

如果以 AND(X, Y) 为例,输入 X 为 false,则输入 Y 无关紧要...在组合散列时,人们可能希望输入很重要。

如果您采用 XOR(X, Y)BOTH 输入 总是 很重要。在 Y 无关紧要的情况下,将没有 X 的值。如果 X 或 Y 发生变化,那么输出将反映这一点。

【讨论】:

【参考方案3】:

覆盖左侧 2 列,并尝试仅使用输出来计算输入。

 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 不会泄露任何关于它的输入。

【讨论】:

【参考方案4】:

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;

您可以搜索其他 *** 问答,以获取有关 31 背后的魔力以及 Java 代码如此频繁使用它的原因的更多信息。它并不完美,但具有非常好的一般性能特征。

【讨论】:

Java 的默认“乘以 31 并添加/累积”哈希加载了冲突(例如,任何 stringstring + "AA" IIRC 冲突),他们很久以前希望他们没有将该算法融入规范。也就是说,使用设置更多位的更大奇数,并添加移位或旋转可以解决该问题。 MurmurHash3 的“混合”就是这样做的。【参考方案5】:

我想向其他找到此页面的人明确指出。 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 &lt;= Min(A, B) 我们知道这一点,因为你无法通过 A 或 B 的 0 位将它们变为 1。所以每个零位都保持零位,每个位都有机会变成零位(因此值更小)。

与:C = A OR B

反之亦然:C &gt;= Max(A, B) 有了这个,我们看到了 AND 函数的推论。任何已经是 1 的位都不能被 OR 变成 0,所以它保持为 1,但每个 0 位都有机会变成 1,因此是一个更大的数。

这意味着输入的状态对输出施加了限制。如果你将任何与 90 相加,你就知道输出将等于或小于 90,而不管其他值是什么。

对于 XOR,没有基于输入的隐含限制。在某些特殊情况下,您会发现,如果您对 255 的字节进行异或运算,则得到的结果是相反的,但任何可能的字节都可以从中输出。每个位都有机会根据另一个操作数中的相同位改变状态。

【讨论】:

可以说ORbitwise maxANDbitwise min Paulo Ebermann 说得很好。很高兴在这里见到你以及 Crypto.SE! I created a filter 包括我标记为cryptography 的所有内容,也更改为旧问题。这样我在这里找到了你的答案。【参考方案6】:

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

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

致谢:上面的组合器是 Bob Jenkins 的提示。

【讨论】:

有时可交换性是一件好事,但异或是一个糟糕的选择即便如此,因为所有匹配项对都会被哈希为零。算术和更好;一对匹配项的哈希将只保留 31 位有用数据而不是 32 位,但这比保留零要好得多。另一种选择可能是将算术和计算为long,然后将上半部分与下半部分重合。 m = 3 实际上是一个不错的选择,并且在许多系统上速度非常快。请注意,对于任何奇数 m 整数乘法是模 2^322^64 并且因此是可逆的,因此您不会丢失任何位。 超越 MaxInt 会发生什么? 应该选择素数而不是任何奇数 @Infinum 组合哈希时不需要。【参考方案7】:

Xor 可能是组合哈希的“默认”方式,但 Greg Hewgill 的回答也说明了为什么它有缺陷: 两个相同哈希值的异或为零。 在现实生活中,相同的哈希值比人们预期的更常见。然后,您可能会发现在这些(不那么罕见的)极端情况下,生成的组合哈希始终相同(零)。哈希冲突会比您预期的要频繁得多。

在一个人为的示例中,您可能正在组合来自您管理的不同网站的用户的哈希密码。不幸的是,大量用户重复使用他们的密码,结果哈希值的惊人比例为零!

【讨论】:

我希望人为的例子永远不会发生,密码应该加盐。【参考方案8】:

如果您XOR 带有偏差输入的随机输入,则输出是随机的。 ANDOR 的情况并非如此。示例:

00101001 异或 00000000 = 00101001 00101001 和 00000000 = 00000000 00101001 或 11111111 = 11111111

正如@Greg Hewgill 提到的,即使两个输入都是随机的,使用ANDOR 也会导致输出有偏差。

我们在更复杂的事情上使用XOR 的原因是,好吧,没有必要:XOR 完美运行,而且速度非常快。

【讨论】:

【参考方案9】:

假设均匀随机(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 有多少个逻辑函数具有这种均匀的输出分布?为什么 XOR 最适合您问题中所述的目的?

【讨论】:

回答练习:从 16 种可能的不同 a XXX b 操作(0, a &amp; b, a &gt; b, a, a &lt; b, b, a % b, a | b, !a &amp; !b, a == b, !b, a &gt;= b, !a, a &lt;= b, !a | !b, 1),以下有 50%-50% 的 0 和 1 分布,假设 a 和 b 有 50%-50% 0 和 1 的分布:a, b, !a, !b, a % b, a == b,i。例如,也可以使用 XOR (EQUIV) 的反义词... 格雷格,这是一个很棒的答案。在我看到你的原始答案并写出我自己的真值表后,灯泡亮了。我考虑了@Massa 关于如何有 6 种合适的操作来维护分发的回答。虽然a, b, !a, !b 将与它们各自的输入具有相同的分布,但您会失去另一个输入的熵。也就是说,XOR 最适合组合哈希的目的,因为我们想从 a 和 b 中捕获熵。 Here is a paper 解释了在每个函数只调用一次的情况下安全地组合散列是不可能的,除非输出的位数少于每个散列值中位数的总和。这表明这个答案是不正确的。 @Massa 我从未见过 % 用于异或或不相等。 作为Yakk points out,异或可能很危险,因为它会为相同的值生成零。这意味着(a,a)(b,b) 都产生零,这在许多(大多数?)情况下大大增加了基于哈希的数据结构中发生冲突的可能性。

以上是关于为啥 XOR 是组合哈希的默认方式?的主要内容,如果未能解决你的问题,请参考以下文章

PowerShell:组合两个哈希表

哈希表 - 按输入方式排序

针对 Scrypt 组合哈希验证 python 密码:(设置+盐+哈希)

Perl:组合两个哈希数组的值并使第二个数组的值成为输出哈希的键

如何在 Perl 中组合哈希?

快速简单的哈希码组合