Perché XOR è il modo predefinito di combinare gli hash?


145

Diciamo che sono due hash H(A)e H(B)e si desidera combinarli. Ho letto che un buon modo per combinare due hash è per XORloro, ad es XOR( H(A), H(B) ).

La migliore spiegazione che ho trovato è brevemente toccata qui in queste linee guida sulla funzione hash :

XORing due numeri con distribuzione approssimativamente casuale comporta un altro numero ancora con distribuzione approssimativamente casuale *, ma che ora dipende dai due valori.
...
* Ad ogni bit dei due numeri da combinare, viene emesso uno 0 se i due bit sono uguali, altrimenti un 1. In altre parole, nel 50% delle combinazioni, verrà emesso un 1. Quindi, se i due bit di ingresso hanno ciascuno circa il 50-50 di probabilità di essere 0 o 1, lo sarà anche il bit di uscita.

Puoi spiegare l'intuizione e / o la matematica dietro perché XOR dovrebbe essere l'operazione predefinita per combinare le funzioni hash (piuttosto che OR o AND ecc.)?


20
Penso che tu l'abbia appena fatto;)
Massa,

22
si noti che XOR può o meno essere un "buon" modo di "combinare" gli hash, a seconda di ciò che si desidera in una "combinazione". XOR è commutativo: XOR (H (A), H (B)) è uguale a XOR (H (B), H (A)). Ciò significa che XOR non è un modo corretto per creare una sorta di hash di una sequenza ordinata di valori, poiché non acquisisce l'ordine.
Thomas Pornin,

6
Oltre al problema con l'ordine (commento sopra), c'è un problema con valori uguali. XOR (H (1), H (1)) = 0 (per qualsiasi funzione H), XOR (H (2), H (2)) = 0 e così via. Per qualsiasi N: XOR (H (N), H (N)) = 0. Valori uguali si verificano abbastanza spesso nelle app reali, significa che il risultato di XOR sarà 0 troppo spesso per essere considerato un buon hash.
Andrei Galatyn,

Cosa usi per la sequenza di valori ordinata? Diciamo che vorrei creare un hash di timestamp o indice. (MSB meno importante dell'LSB). Scusa se questa discussione è vecchia di 1 anno.
Alexis,

Risposte:


120

Supponendo che gli ingressi siano uniformemente casuali (1 bit), la distribuzione della probabilità di uscita della funzione AND è del 75% 0e 25% 1. Al contrario, OR è del 25% 0e 75% 1.

La funzione XOR è 50% 0e 50% 1, quindi è buona per combinare distribuzioni di probabilità uniformi.

Questo può essere visto scrivendo tabelle di verità:

 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

Esercizio: Come molte funzioni logiche di due ingressi 1-bit ae bhanno questa distribuzione uniforme di uscita? Perché XOR è il più adatto allo scopo indicato nella tua domanda?


24
rispondendo all'esercizio: tra le 16 possibili diverse operazioni 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), le seguenti hanno distribuzioni del 50% -50% di 0 e 1 s, supponendo che aeb abbiano distribuzioni del 50% -50% di 0 e 1 s: a, b, !a, !b, a % b, a == bcioè il contrario di XOR (EQUIV) avrebbe potuto essere usato anche ...
Massa,

7
Greg, questa è una risposta fantastica. La lampadina si è accesa per me dopo aver visto la tua risposta originale e scritto le mie tabelle di verità. Ho considerato la risposta di @ Massa su come ci sono 6 operazioni adatte per mantenere la distribuzione. E mentre a, b, !a, !bavrà la stessa distribuzione dei rispettivi input, perderai l'entropia dell'altro input. Cioè, XOR è più adatto allo scopo di combinare gli hash perché vogliamo catturare l'entropia sia da a che da b.
Nate Murray,

1
Ecco un documento che spiega che la combinazione sicura di hash in cui ogni funzione viene chiamata una sola volta non è possibile senza produrre meno bit della somma del numero di bit in ciascun valore di hash. Ciò suggerisce che questa risposta non è corretta.
Tamás Szelei,

3
@Massa Non ho mai visto% usato per XOR o non uguale.
Buge,

7
Come sottolinea Yakk , XOR può essere pericoloso in quanto produce zero per valori identici. Ciò significa (a,a)che (b,b)entrambi producono zero, il che nella maggior parte dei casi (la maggior parte?) Aumenta notevolmente la probabilità di collisioni nelle strutture di dati basate sull'hash.
Drew Noakes,

170

xorè una pericolosa funzione predefinita da utilizzare durante l'hash. È meglio di ande or, ma questo non dice molto.

xorè simmetrico, quindi l'ordine degli elementi viene perso. Così"bad" hash combina lo stesso di "dab".

xor associa valori identici a coppie a zero e dovresti evitare di mappare valori "comuni" a zero:

Così (a,a) viene mappato su 0 e(b,b) anche mappato su 0. Poiché tali coppie sono quasi sempre più comuni di quanto la casualità possa implicare, si finisce con molte collisioni a zero di quanto si dovrebbe.

Con questi due problemi, xorfinisce per essere un combinatore di hash che sembra mezzo decente in superficie, ma non dopo un'ulteriore ispezione.

Su hardware moderno, l'aggiunta di solito è più veloce di xor(probabilmente utilizza più potenza per farlo, lo ammetto). L'aggiunta della tabella di verità è simile axor quella del bit in questione, ma invia anche un bit al bit successivo quando entrambi i valori sono 1. Ciò significa che cancella meno informazioni.

Quindi hash(a) + hash(b)è meglio che hash(a) xor hash(b)in questo se a==b, il risultato èhash(a)<<1 invece di 0.

Questo rimane simmetrico; quindi lo "bad"e "dab"ottenere lo stesso risultato rimane un problema. Possiamo rompere questa simmetria per un costo modesto:

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

aka hash(a)*3 + hash(b). ( hash(a)si consiglia di calcolare una volta e memorizzare se si utilizza la soluzione a turni). Qualsiasi costante dispari invece di 3mappare biiettivamente un kintero senza segno " -bit" su se stesso, poiché la mappa su numeri interi senza segno è un modulo di matematica 2^kper alcuni k, e qualsiasi costante dispari è relativamente primaria 2^k.

Per una versione ancora più elaborata, possiamo esaminare boost::hash_combine, che è effettivamente:

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

qui sommiamo alcune versioni spostate di seedcon una costante (che è sostanzialmente casuale 0s e 1s - in particolare è l'inverso del rapporto aureo come una frazione di punto fisso a 32 bit) con qualche aggiunta e uno xor. Ciò interrompe la simmetria e introduce un po 'di "rumore" se i valori di hash in entrata sono scarsi (cioè immagina che ogni componente abbia l'hash su 0 - quanto sopra lo gestisce bene, generando una sbavatura di 1e 0s dopo ogni combinazione. La mia ingenua 3*hash(a)+hash(b)semplicemente genera un 0in questo caso).

(Per coloro che non hanno familiarità con C / C ++, a size_tè un valore intero senza segno che è abbastanza grande da descrivere la dimensione di qualsiasi oggetto in memoria. Su un sistema a 64 bit, di solito è un numero intero senza segno a 64 bit. Su un sistema a 32 bit , un numero intero senza segno a 32 bit.)


Bella risposta Yakk. Questo algoritmo funziona ugualmente bene su entrambi i sistemi a 32 e 64 bit? Grazie.
Dave,

1
@dave aggiunge più bit a 0x9e3779b9.
Yakk - Adam Nevraumont,

10
OK, per essere completo ... ecco la costante di precisione a 64 bit (calcolata con double doppi e long long senza segno): 0x9e3779b97f4a7c16. È interessante notare che è ancora uniforme. Ri-fare lo stesso calcolo usando PI invece del Rapporto aureo produce: 0x517cc1b727220a95 che è dispari, anziché pari, quindi probabilmente "più primo" dell'altra costante. Ho usato: std :: cout << std :: hex << (unsigned long long) ((1.0L / 3.141592653589793238462643383279502884197169399337510L) * (powl (2.0L, 64.0L))) << std :: endl; con cout.precision (numeric_limits <long double> :: max_digits10); Grazie ancora Yakk.
Dave,

2
@Dave la regola del rapporto aureo inverso per questi casi è il primo numero dispari uguale o maggiore del calcolo che stai facendo. Quindi aggiungi solo 1. È un numero importante perché la sequenza di N * il rapporto, mod la dimensione massima (2 ^ 64 qui) posiziona il valore successivo nella sequenza esattamente a quel rapporto nel mezzo del più grande 'gap' in numeri. Cerca nel web "hashing Fibonacci" per maggiori informazioni.
Scott Carey,

1
@Dave il numero giusto sarebbe 0.9E3779B97F4A7C15F39 ... Vedi link . Potresti soffrire della regola del round-to-even (che è buono per i contabili), o semplicemente, se inizi con una costante sqrt (5) letterale, quando sottrai 1, rimuovi il bit di ordine superiore, un po 'deve essere stato perso.
migle

29

Nonostante le sue utili proprietà di missaggio dei bit, XOR non lo è un buon modo per combinare gli hash a causa della sua commutatività. Considera cosa accadrebbe se memorizzassi le permutazioni di {1, 2, ..., 10} in una tabella hash di 10 tuple.

Una scelta molto migliore è m * H(A) + H(B), dove m è un numero dispari grande.

Ringraziamento: il combinatore sopra era un suggerimento di Bob Jenkins.


2
A volte la commutatività è una buona cosa, ma xor è una scelta pessima anche allora perché tutte le coppie di oggetti corrispondenti verranno portate a zero. Una somma aritmetica è migliore; l'hash di una coppia di elementi corrispondenti conserverà solo 31 bit di dati utili anziché 32, ma è molto meglio che mantenere zero. Un'altra opzione potrebbe essere quella di calcolare la somma aritmetica come a longe quindi reinserire la parte superiore con la parte inferiore.
supercat

1
m = 3è in realtà una buona scelta e molto veloce su molti sistemi. Nota che per ogni mmoltiplicazione di numeri dispari è modulo 2^32o 2^64è quindi invertibile, quindi non stai perdendo alcun bit.
StefanKarpinski,

Cosa succede quando vai oltre MaxInt?
dirompente il

2
invece di qualsiasi numero dispari uno dovrebbe scegliere un numero primo
TermoTux,

2
@Infinum non è necessario quando si combinano gli hash.
Marcelo Cantos,

17

Xor potrebbe essere il modo "predefinito" di combinare gli hash, ma la risposta di Greg Hewgill mostra anche perché ha i suoi problemi: lo xor di due valori di hash identici è zero. Nella vita reale, ci sono hash identici più comuni di quanto ci si potrebbe aspettare. Potresti quindi scoprire che in questi casi angolari (non così rari), gli hash combinati risultanti sono sempre gli stessi (zero). Le collisioni di hashish sarebbero molto, molto più frequenti di quanto ti aspetti.

In un esempio inventato, potresti combinare password con hash degli utenti di diversi siti Web che gestisci. Sfortunatamente, un gran numero di utenti riutilizza le proprie password e una percentuale sorprendente degli hash risultanti è zero!


Spero che l'esempio inventato non accada mai, le password dovrebbero essere salate.
user60561

8

C'è qualcosa che voglio sottolineare esplicitamente per gli altri che trovano questa pagina. AND e OR limitano l'output come BlueRaja - Danny Pflughoe sta cercando di sottolineare, ma può essere meglio definito:

Per prima cosa voglio definire due semplici funzioni che userò per spiegare questo: Min () e Max ().

Min (A, B) restituirà il valore più piccolo tra A e B, ad esempio: Min (1, 5) restituisce 1.

Max (A, B) restituirà il valore maggiore tra A e B, ad esempio: Max (1, 5) restituisce 5.

Se ti viene dato: C = A AND B

Quindi puoi trovarlo C <= Min(A, B) lo sappiamo perché non c'è nulla che puoi E con gli 0 bit di A o B per renderli 1s. Quindi ogni bit zero rimane zero e ogni bit ha la possibilità di diventare un bit zero (e quindi un valore più piccolo).

Con: C = A OR B

È vero il contrario: C >= Max(A, B)con questo vediamo il corollario della funzione AND. Qualsiasi bit che è già uno non può essere impostato su OR come zero, quindi rimane uno, ma ogni bit zero ha la possibilità di diventare uno, e quindi un numero maggiore.

Ciò implica che lo stato dell'input applica restrizioni sull'output. Se ANDI qualcosa con 90, sai che l'output sarà uguale o inferiore a 90 indipendentemente dall'altro valore.

Per XOR, non vi sono restrizioni implicite basate sugli input. Ci sono casi speciali in cui puoi scoprire che se si XOR un byte con 255 di quello che si ottiene l'inverso, ma qualsiasi byte possibile può essere emesso da quello. Ogni bit ha la possibilità di cambiare stato a seconda dello stesso bit nell'altro operando.


6
Si potrebbe dire che ORè bit per bit massimo , ed ANDè bit per bit min .
Paŭlo Ebermann,

Molto ben affermato Paulo Ebermann. Piacere di vederti qui così come Crypto.SE!
Corey Ogburn,

Ho creato un filtro che include tutto ciò che è stato taggato con crittografia , anche modifiche a vecchie domande. In questo modo ho trovato la tua risposta qui.
Paŭlo Ebermann,

3

Se si XORdispone di un input casuale con un input distorto, l'output è casuale. Lo stesso non è vero per ANDo OR. Esempio:

00101001 XOR 00000000 = 00101001
00101001 E 00000000 = 00000000
00101001 OR 11111111 = 11111111

Come menziona @Greg Hewgill, anche se entrambi gli input sono casuali, l'utilizzo ANDo ORgenererà un output distorto.

La ragione per cui usiamo XORqualcosa di più complesso è che, beh, non è necessario: XORfunziona perfettamente ed è incredibilmente stupido-veloce.


1

Copri le 2 colonne a sinistra e prova a capire quali input stanno usando solo l'output.

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

Quando hai visto un 1 bit avresti dovuto capire che entrambi gli ingressi erano 1.

Ora fai lo stesso per XOR

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

XOR non concede nulla al riguardo.


0

Il codice sorgente per varie versioni di hashCode()in java.util.Arrays è un ottimo riferimento per algoritmi di hashing solidi e di uso generale. Sono facilmente comprensibili e tradotti in altri linguaggi di programmazione.

In parole povere, la maggior parte delle hashCode()implementazioni multi-attributo seguono questo schema:

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

Puoi cercare altre domande e risposte StackOverflow per ulteriori informazioni sulla magia che sta dietro 31e sul perché il codice Java lo utilizza così frequentemente. È imperfetto, ma ha ottime caratteristiche generali di prestazione.


2
L'hash predefinito "moltiplica per 31 e aggiungi / accumula" di Java è carico di collisioni (ad es. Eventuali stringcollisioni con string + "AA"IIRC) e molto tempo fa desideravano non aver inserito quell'algoritmo nelle specifiche. Detto questo, l'utilizzo di un numero dispari maggiore con più bit impostati e l'aggiunta di turni o rotazioni risolve il problema. Il 'mix' di MurmurHash3 fa questo.
Scott Carey,

0

XOR non ignora alcuni degli input a volte come OR e AND .

Se prendi AND (X, Y) per esempio, e inserisci input X con false, allora l'ingresso Y non ha importanza ... e probabilmente si vorrebbe che l'input contasse quando si combinano gli hash.

Se si prende XOR (X, Y) allora ENTRAMBI ingressi SEMPRE materia. Non ci sarebbe alcun valore di X dove Y non ha importanza. Se si modifica X o Y, l'output rifletterà quello.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.