¿Por qué es XOR la ​​forma predeterminada de combinar hashes?

145

Digamos que tiene dos hashes H(A)y H(B)quiere combinarlos. He leído que una buena forma de combinar dos hashes es para XORellos, por ejemplo XOR( H(A), H(B) ).

La mejor explicación que he encontrado se trata brevemente aquí en estas pautas de función hash :

XORing dos números con distribución más o menos aleatoria da como resultado otro número aún con distribución más o menos aleatoria *, pero que ahora depende de los dos valores.
...
* En cada bit de los dos números a combinar, se emite un 0 si los dos bits son iguales, de lo contrario un 1. En otras palabras, en el 50% de las combinaciones, se emitirá un 1. Entonces, si los dos bits de entrada tienen cada uno una probabilidad de 50-50 de ser 0 o 1, entonces también lo tendrá el bit de salida.

¿Puede explicar la intuición y / o las matemáticas detrás de por qué XOR debería ser la operación predeterminada para combinar funciones hash (en lugar de OR o AND, etc.)?

Nate Murray
fuente
20
Creo que acabas de hacerlo;)
Massa
22
tenga en cuenta que XOR puede o no ser una "buena" forma de "combinar" hashes, dependiendo de lo que desee en una "combinación". XOR es conmutativo: XOR (H (A), H (B)) es igual a XOR (H (B), H (A)). Esto significa que XOR no es una forma adecuada de crear una especie de hash de una secuencia ordenada de valores, ya que no captura el orden.
Thomas Pornin 05 de
66
Además del problema con el orden (comentario anterior), hay un problema con los valores iguales. XOR (H (1), H (1)) = 0 (para cualquier función H), XOR (H (2), H (2)) = 0 y así sucesivamente. Para cualquier N: XOR (H (N), H (N)) = 0. Los valores iguales ocurren con bastante frecuencia en aplicaciones reales, lo que significa que el resultado de XOR será 0 con demasiada frecuencia para ser considerado como un buen hash.
Andrei Galatyn
¿Qué utiliza para la secuencia ordenada de valores? Digamos que me gustaría crear un hash de marca de tiempo o índice. (MSB menos importante que LSB). Lo siento si este hilo tiene 1 año.
Alexis

Respuestas:

120

Suponiendo entradas uniformemente aleatorias (1 bit), la distribución de probabilidad de salida de la función AND es 75% 0y 25% 1. Por el contrario, OR es 25% 0y 75% 1.

La función XOR es 50% 0y 50% 1, por lo tanto, es buena para combinar distribuciones de probabilidad uniformes.

Esto se puede ver escribiendo tablas de verdad:

 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

Ejercicio: ¿Cómo muchas funciones lógicas de dos entradas de 1 bit ay btienen esta distribución uniforme de salida? ¿Por qué XOR es el más adecuado para el propósito indicado en su pregunta?

Greg Hewgill
fuente
24
respondiendo al ejercicio: de las 16 posibles operaciones diferentes de 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), las siguientes tienen 50% -50% de distribuciones de 0s y 1s, suponiendo que ayb tienen 50% -50% de distribuciones de 0s y 1s: a, b, !a, !b, a % b, a == bes decir, lo contrario de XOR (EQUIV) podría haber sido utilizado también ...
Massa
77
Greg, esta es una respuesta increíble. La bombilla se encendió para mí después de que vi su respuesta original y escribí mis propias tablas de verdad. Consideré la respuesta de @ Massa sobre cómo hay 6 operaciones adecuadas para mantener la distribución. Y aunque a, b, !a, !btendrá la misma distribución que sus respectivas entradas, perderá la entropía de la otra entrada. Es decir, XOR es el más adecuado para combinar hashes porque queremos capturar entropía tanto de a como de b.
Nate Murray
1
Aquí hay un documento que explica que combinar hashes de forma segura donde cada función se llama solo una vez no es posible sin generar menos bits que la suma de la cantidad de bits en cada valor de hash. Esto sugiere que esta respuesta no es correcta.
Tamás Szelei
3
@Massa Nunca he visto% usado para XOR o no igual.
Buge
77
Como señala Yakk , XOR puede ser peligroso ya que produce cero para valores idénticos. Este medio (a,a)y (b,b)ambos productos cero, que en muchos (la mayoría?) De los casos aumenta en gran medida la probabilidad de colisiones en las estructuras de datos basados en hash.
Drew Noakes
170

xores una función predeterminada peligrosa para usar cuando se usa hashing. Es mejor que andy or, pero eso no dice mucho.

xores simétrico, por lo que se pierde el orden de los elementos. Entonces "bad", el hash combinará lo mismo que "dab".

xor asigna valores idénticos por pares a cero, y debe evitar asignar valores "comunes" a cero:

Por lo tanto, (a,a)se asigna a 0, y (b,b)también se asigna a 0. Como tales pares son casi siempre más comunes de lo que podría implicar la aleatoriedad, terminas con muchas colisiones en cero de lo que deberías.

Con estos dos problemas, xortermina siendo un combinador de hash que parece medio decente en la superficie, pero no después de una inspección adicional.

En el hardware moderno, agregar generalmente casi tan rápido como xor(probablemente use más potencia para lograr esto, es cierto). Agregar la tabla de verdad es similar al xorbit en cuestión, pero también envía un bit al siguiente bit cuando ambos valores son 1. Esto significa que borra menos información.

Entonces hash(a) + hash(b)es mejor que hash(a) xor hash(b)en eso si a==b, el resultado es en hash(a)<<1lugar de 0.

Esto sigue siendo simétrico; por lo que el "bad"y "dab"conseguir el mismo resultado sigue siendo un problema. Podemos romper esta simetría por un costo modesto:

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

aka hash(a)*3 + hash(b). ( hash(a)se recomienda calcular una vez y almacenar si usa la solución de turno). Cualquier constante impar en lugar de mapeará bijetivamente un entero sin signo 3" k-bit" consigo mismo, ya que el mapa en enteros sin signo es un módulo matemático 2^kpara algunos k, y cualquier constante impar es relativamente primo 2^k.

Para una versión aún más elegante, podemos examinar boost::hash_combine, que es efectivamente:

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

aquí agregamos algunas versiones desplazadas de seedcon una constante (que es básicamente 0s y 1s al azar , en particular, es la inversa de la proporción áurea como una fracción de punto fijo de 32 bits) con alguna suma y un xor. Esto rompe la simetría, e introduce un poco de "ruido" si los valores hash entrantes son pobres (es decir, imaginar cada hashes componentes a 0 - las manijas por encima de ella, así, generar una mancha de 1y 0. S después de cada combinar mi ingenua 3*hash(a)+hash(b)simplemente generan una 0en Ese caso).

(Para aquellos que no están familiarizados con C / C ++, a size_tes un valor entero sin signo que es lo suficientemente grande como para describir el tamaño de cualquier objeto en la memoria. En un sistema de 64 bits, generalmente es un entero sin signo de 64 bits. En un sistema de 32 bits , un entero sin signo de 32 bits).

Yakk - Adam Nevraumont
fuente
Buena respuesta Yakk. ¿Este algoritmo funciona igualmente bien en sistemas de 32 bits y 64 bits? Gracias.
Dave
1
@dave agrega más bits a 0x9e3779b9.
Yakk - Adam Nevraumont
10
OK, para completar ... aquí está la constante de 64 bits de precisión completa (calculada con dobles largos y largos largos sin signo): 0x9e3779b97f4a7c16. Curiosamente, aún es uniforme. Volver a hacer el mismo cálculo usando PI en lugar de la proporción áurea produce: 0x517cc1b727220a95 que es impar, en lugar de par, por lo tanto, probablemente "más primo" que la otra constante. Solía: std :: cout << std :: hex << (unsigned long long) ((1.0L / 3.14159265358979323846264338327950288419716939937510L) * (powl (2.0L, 64.0L))) << std :: endl; con cout.precision (numeric_limits <long double> :: max_digits10); Gracias de nuevo Yakk.
Dave
2
@Dave la regla inversa de la proporción áurea para estos casos es el primer número impar igual o mayor que el cálculo que está haciendo. Así que solo agregue 1. Es un número importante porque la secuencia de N * la relación, modifique el tamaño máximo (2 ^ 64 aquí) coloca el siguiente valor en la secuencia exactamente en esa relación en el medio de la 'brecha' más grande en números. Busque en la web "Hash de Fibonacci" para obtener más información.
Scott Carey
1
@Dave el número correcto sería 0.9E3779B97F4A7C15F39 ... Ver enlace . Podría estar sufriendo la regla de redondeo a par (que es bueno para los contadores), o simplemente, si comienza con una constante literal sqrt (5), cuando resta 1, elimina el bit de orden superior, un Debe haberse perdido un poco.
migle
29

A pesar de sus prácticas propiedades de mezcla de bits, XOR no es una buena forma de combinar hashes debido a su conmutatividad. Considere lo que sucedería si almacenara las permutaciones de {1, 2, ..., 10} en una tabla hash de 10 tuplas.

Una opción mucho mejor es m * H(A) + H(B), donde m es un número impar grande.

Crédito: El combinador anterior fue un consejo de Bob Jenkins.

Marcelo Cantos
fuente
2
A veces, la conmutatividad es algo bueno, pero xor es una mala elección, incluso porque todos los pares de elementos coincidentes se convertirán en cero. Una suma aritmética es mejor; el hash de un par de elementos coincidentes retendrá solo 31 bits de datos útiles en lugar de 32, pero eso es mucho mejor que retener cero. Otra opción puede ser calcular la suma aritmética como longay luego volver a juntar la parte superior con la parte inferior.
supercat
1
m = 3en realidad es una buena opción y muy rápida en muchos sistemas. Tenga en cuenta que para cualquier número mentero impar, la multiplicación es módulo 2^32o, 2^64por lo tanto, es invertible, por lo que no está perdiendo ningún bit.
StefanKarpinski
¿Qué sucede cuando vas más allá de MaxInt?
disruptivo
2
en lugar de cualquier número impar, uno debería elegir un primo
TermoTux
2
@Infinum que no es necesario cuando se combinan hashes.
Marcelo Cantos
17

Xor puede ser la forma "predeterminada" de combinar hashes, pero la respuesta de Greg Hewgill también muestra por qué tiene sus dificultades: el xor de dos valores hash idénticos es cero. En la vida real, hay hashes idénticos que son más comunes de lo que cabría esperar. Entonces puede encontrar que en estos casos de esquina (no tan infrecuentes), los hashes combinados resultantes son siempre los mismos (cero). Las colisiones de hash serían mucho, mucho más frecuentes de lo que esperas.

En un ejemplo artificial, puede estar combinando contraseñas hash de usuarios de diferentes sitios web que administra. Desafortunadamente, un gran número de usuarios reutiliza sus contraseñas, ¡y una proporción sorprendente de los hashes resultantes son cero!

Leo Goodstadt
fuente
Espero que el ejemplo artificial nunca suceda, las contraseñas deben ser saladas.
user60561
8

Hay algo que quiero señalar explícitamente para otros que encuentran esta página. AND y OR restringen la salida como BlueRaja - Danny Pflughoe está tratando de señalar, pero se puede definir mejor:

Primero quiero definir dos funciones simples que usaré para explicar esto: Min () y Max ().

Min (A, B) devolverá el valor más pequeño entre A y B, por ejemplo: Min (1, 5) devuelve 1.

Max (A, B) devolverá el valor que es mayor entre A y B, por ejemplo: Max (1, 5) devuelve 5.

Si le dan: C = A AND B

Entonces puedes encontrar que C <= Min(A, B)sabemos esto porque no hay nada que puedas Y con los 0 bits de A o B para convertirlos en 1s. Por lo tanto, cada bit cero permanece en un bit cero y cada bit tiene la posibilidad de convertirse en un bit cero (y, por lo tanto, en un valor menor).

Con: C = A OR B

Lo contrario es cierto: C >= Max(A, B)con esto, vemos el corolario de la función AND. Cualquier bit que ya sea uno no puede ORingarse para que sea un cero, por lo que permanece como uno, pero cada bit cero tiene la posibilidad de convertirse en uno y, por lo tanto, en un número mayor.

Esto implica que el estado de la entrada aplica restricciones en la salida. Si usted Y cualquier cosa con 90, sabe que la salida será igual o menor a 90 independientemente de cuál sea el otro valor.

Para XOR, no hay restricción implícita basada en las entradas. Hay casos especiales en los que puede encontrar que si XOR un byte con 255, obtiene el inverso, pero cualquier byte posible se puede generar a partir de eso. Cada bit tiene la oportunidad de cambiar de estado dependiendo del mismo bit en el otro operando.

Corey Ogburn
fuente
66
Se podría decir que ORes máximo a nivel de bits , y ANDes bit a bit min .
Paŭlo Ebermann
Muy bien dicho Paulo Ebermann. Es bueno verte aquí, así como Crypto.SE!
Corey Ogburn
Creé un filtro que me incluye todo lo relacionado con la criptografía , también cambia a viejas preguntas. De esta manera encontré tu respuesta aquí.
Paŭlo Ebermann
3

Si tiene XORuna entrada aleatoria con una entrada sesgada, la salida es aleatoria. Lo mismo no es cierto para ANDo OR. Ejemplo:

00101001 XOR 00000000 = 00101001
00101001 Y 00000000 = 00000000
00101001 O 11111111 = 11111111

Como @Greg Hewgill menciona, incluso si ambas entradas son aleatorias, usar ANDo ORdará como resultado una salida sesgada.

La razón por la que usamos XORsobre algo más complejo es que, bueno, no hay necesidad: XORfunciona perfectamente y es increíblemente rápido.

BlueRaja - Danny Pflughoeft
fuente
1

Cubra las 2 columnas de la izquierda e intente averiguar qué están usando las entradas solo como salida.

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

Cuando vio un 1 bit, debería haber deducido que ambas entradas eran 1.

Ahora haz lo mismo para XOR

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

XOR no regala nada sobre sus entradas.

Robert
fuente
0

El código fuente para varias versiones de hashCode()en java.util.Arrays es una gran referencia para, uso en algoritmos de hash sólidos. Se entienden y traducen fácilmente a otros lenguajes de programación.

En términos generales, la mayoría de las hashCode()implementaciones de múltiples atributos siguen este patrón:

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;
}

Puede buscar otras preguntas y respuestas de StackOverflow para obtener más información sobre la magia detrás 31y por qué el código Java lo usa con tanta frecuencia. Es imperfecto, pero tiene muy buenas características generales de rendimiento.

kevinarpe
fuente
2
El hash predeterminado de "multiplicar por 31 y agregar / acumular" de Java está cargado de colisiones (por ejemplo, cualquier stringcolisión con string + "AA"IIRC) y hace mucho tiempo desearon no haber incorporado ese algoritmo en la especificación. Dicho esto, el uso de un número impar más grande con más bits establecidos y la adición de turnos o rotaciones corrige ese problema. La 'mezcla' de MurmurHash3 hace esto.
Scott Carey
0

XOR no ignora algunas de las entradas, a veces como OR y AND .

Si toma AND (X, Y) por ejemplo, y alimenta la entrada X con falso, entonces la entrada Y no importa ... y uno probablemente querría que la entrada importara al combinar hashes.

Si toma XOR (X, Y) , AMBAS entradas SIEMPRE importan. No habría valor de X donde Y no importa. Si se cambia X o Y, la salida reflejará eso.

Sunsetquest
fuente