2012-01-02 11 views
6

Sto tentando di implementare un attacco di collisione sugli hash (sto visitando il corso "crittografia"). Pertanto ho due matrici di hash (= sequenze di byte byte[]) e voglio trovare gli hash che sono presenti in entrambi gli array. Dopo alcune ricerche e un sacco di pensiero sono sicuro che la soluzione migliore su una macchina single-core sarebbe un HashSet (aggiungi tutti gli elementi del primo array e verifica tramite contains se sono già presenti elementi del secondo array).Come trovare byte identici [] - oggetti in due array contemporaneamente?

Tuttavia, voglio implementare una soluzione concorrente, dal momento che ho accesso a una macchina con 8 core e 12 GB di RAM. La soluzione migliore a cui posso pensare è ConcurrentHashSet, che potrebbe essere creata tramite Collections.newSetFromMap(new ConcurrentHashMap<A,B>()). Usando questa struttura dati potrei aggiungere tutti gli elementi del primo array in parallelo e - dopo tutti gli elementi dove sono stati aggiunti - posso verificare contemporaneamente tramite contains per gli hash identici.

Quindi la mia domanda è: Conosci un algoritmo progettato per questo problema esatto? In caso contrario, si ha esperienza nell'utilizzo di un tale ConcurrentHashSet in merito a problemi e complessità effettiva del runtime? Oppure puoi consigliare un'altra struttura di dati precostruita che potrebbe aiutarmi?

PS: Se qualcuno è interessato ai dettagli: ho intenzione di utilizzare Skandium per parallelizzare il mio programma.

+0

Gli array sono già ordinati? In tal caso, una funzione di fusione dell'unione unica troverà i duplicati. Altrimenti potresti ordinare array1 e array2 in parallelo e fare unire i risultati. – Ingo

+1

Con gli hash del byte vuoi dire che tutti gli hash sono nell'intervallo 0-255? – Tudor

+0

Intendevo le sequenze di byte, ad esempio 'byte []'. Sono il risultato di una funzione hash come SHA o MD5. No, gli array non sono ordinati. Ordinare e unirle richiede O (n log n) per l'ordinamento e O (n + m) per l'unione. Speravo in una maggiore efficienza. –

risposta

5

Penso che sarebbe una completa perdita di tempo utilizzare qualsiasi forma di HashMap. Sto indovinando che stai calcolando hash multi-byte di vari dati, questi sono già hash es, non è necessario eseguire altro hashing su di essi.

Anche se non lo si specifica, suppongo che i propri hash siano sequenze byte. Chiaramente uno trie o uno dawg sarebbe l'ideale per archiviarli.

Suggerirei pertanto di implementare uno trie/dawg e utilizzarlo per memorizzare tutti gli hash nel primo array. È quindi possibile utilizzare tutta la potenza di calcolo in parallelo per cercare ogni elemento nel secondo array in questo trie. Non sarebbero richieste serrature.

Aggiunto

Ecco una semplice implementazione Dawg bussai insieme. Sembra funzionare.

public class Dawg { 
    // All my children. 
    Dawg[] children = new Dawg[256]; 
    // Am I a leaf. 
    boolean isLeaf = false; 

    // Add a new word. 
    public void add (byte[] word) { 
    // Finds its location, growing as necessary. 
    Dawg loc = find (word, 0, true); 
    loc.isLeaf = true; 
    } 

    // String form. 
    public void add (String word) { 
    add(word.getBytes()); 
    } 

    // Returns true if word is in the dawg. 
    public boolean contains (byte [] word) { 
    // Finds its location, no growing allowed. 
    Dawg d = find (word, 0, false); 
    return d != null && d.isLeaf; 
    } 

    // String form. 
    public boolean contains (String word) { 
    return contains(word.getBytes()); 
    } 

    // Find the Dawg - growing the tree as necessary if requested. 
    private Dawg find (byte [] word, int i, boolean grow) { 
    Dawg child = children[word[i]]; 
    if (child == null) { 
     // Not present! 
     if (grow) { 
     // Grow the tree. 
     child = new Dawg(); 
     children[word[i]] = child; 
     } 
    } 
    // Found it? 
    if (child != null) { 
     // More to find? 
     if (i < word.length - 1) { 
     child = child.find(word, i+1, grow); 
     } 
    } 
    return child; 
    } 

    public static void main (String[] args) { 
    Dawg d = new Dawg(); 
    d.add("H"); 
    d.add("Hello"); 
    d.add("World"); 
    d.add("Hell"); 
    System.out.println("Hello is "+(d.contains("Hello")?"in":"out")); 
    System.out.println("World is "+(d.contains("World")?"in":"out")); 
    System.out.println("Hell is "+(d.contains("Hell")?"in":"out")); 
    System.out.println("Hal is "+(d.contains("Hal")?"in":"out")); 
    System.out.println("Hel is "+(d.contains("Hel")?"in":"out")); 
    System.out.println("H is "+(d.contains("H")?"in":"out")); 
    } 
} 

Aggiunto

Questo potrebbe essere un buon punto di partenza in una versione senza blocchi concorrente. Queste cose sono notoriamente difficili da testare quindi non posso garantire che funzionerà, ma a mio avviso certamente dovrebbe.

import java.util.concurrent.atomic.AtomicReferenceArray; 


public class LFDawg { 
    // All my children. 
    AtomicReferenceArray<LFDawg> children = new AtomicReferenceArray<LFDawg> (256); 
    // Am I a leaf. 
    boolean isLeaf = false; 

    // Add a new word. 
    public void add (byte[] word) { 
    // Finds its location, growing as necessary. 
    LFDawg loc = find(word, 0, true); 
    loc.isLeaf = true; 
    } 

    // String form. 
    public void add (String word) { 
    add(word.getBytes()); 
    } 

    // Returns true if word is in the dawg. 
    public boolean contains (byte[] word) { 
    // Finds its location, no growing allowed. 
    LFDawg d = find(word, 0, false); 
    return d != null && d.isLeaf; 
    } 

    // String form. 
    public boolean contains (String word) { 
    return contains(word.getBytes()); 
    } 

    // Find the Dawg - growing the tree as necessary if requested. 
    private LFDawg find (byte[] word, int i, boolean grow) { 
    LFDawg child = children.get(word[i]); 
    if (child == null) { 
     // Not present! 
     if (grow) { 
     // Grow the tree. 
     child = new LFDawg(); 
     if (!children.compareAndSet(word[i], null, child)) { 
      // Someone else got there before me. Get the one they set. 
      child = children.get(word[i]); 
     } 
     } 
    } 
    // Found it? 
    if (child != null) { 
     // More to find? 
     if (i < word.length - 1) { 
     child = child.find(word, i + 1, grow); 
     } 
    } 
    return child; 
    } 

    public static void main (String[] args) { 
    LFDawg d = new LFDawg(); 
    d.add("H"); 
    d.add("Hello"); 
    d.add("World"); 
    d.add("Hell"); 
    System.out.println("Hello is " + (d.contains("Hello") ? "in" : "out")); 
    System.out.println("World is " + (d.contains("World") ? "in" : "out")); 
    System.out.println("Hell is " + (d.contains("Hell") ? "in" : "out")); 
    System.out.println("Hal is " + (d.contains("Hal") ? "in" : "out")); 
    System.out.println("Hel is " + (d.contains("Hel") ? "in" : "out")); 
    System.out.println("H is " + (d.contains("H") ? "in" : "out")); 
    } 
} 
+1

Sì, hai ragione, vorrei hash hash che suona terribile. Ma non potevo pensare a un altro modo usando strutture dati predefinite. Ho pensato anche a Tries, ma hanno una ricerca in O (log n) piuttosto O (1) un HashSet ha - o mi sbaglio? Inoltre, se riesco a scavalcare il metodo hash di HashSet, potrei inserire i miei dati direttamente in esso, impedendo l'hashing degli hash. (Ma non riuscivo a vedere come farlo nel JavaDoc di HashSet.) –

+1

@FlorianPilz il (peggiore) tempo di accesso di un Trie è davvero O (log n), dove n = numero di "caratteri" nel tuo " parola". Ma dal momento che gli hash hanno tutti la stessa lunghezza, ciò è irrilevante, poiché n è sempre lo stesso. Inoltre, tieni presente che O (1) è permesso di prendere più a lungo di anche O (e^n) per la piccola n e solo l'asintoto che fa parte della notazione O(). –

+1

@nd Grazie per il tuo commento. Se ti capisco bene, il Trie dovrebbe avere O (1) best-case e worst-case, poiché la lunghezza delle mie parole è costante. Dopo un po 'di lettura, capisco che HashMap e Trie sono paragonabili in termini di velocità (specialmente in questo scenario), quindi Paul ha ragione: un Trie sarebbe meglio, dato che io non perdo velocità, ma salvo memoria e ho una peggiore ipotesi migliore complessità di runtime. Se ho capito bene, questa soluzione fornisce una complessità di runtime garantita O (2 * n), se gli array possono contenere n hash. Corretta? –

0

Un approccio più semplice sarebbe quella di dividere solo il primo array in n parti uguali (o quasi uguali) (con 8 core, n = 8 sembra ragionevole). Quindi risolvi il programma nel modo "normale", osservando se sono presenti alcuni hash nel 2 ° array negli N sub-first-array più piccoli. Questo può essere fatto in parallelo.

Detto questo, non ho mai sentito di tentativi/dawgs prima e ho trovato la discussione principale affascinante e informativo.(Io lavoro principalmente con i numeri, non con le parole)

Ciò presuppone che gli hash del byte [] siano di una lunghezza finita e ridotta in modo tale da poter realmente dividere il file originale da elaborare in parallelo. È questo il caso?

EDIT AGGIUNTO

Per un esempio di questa idea, vedi GPU grafiche Gems, a cura di Wen-Mei W. Hwu, capitolo 11, un articolo di Ligowski, Rudnicki, Liu e Schmidt. Parallelizzano un'enorme ricerca nel database delle sequenze proteiche suddividendo l'enorme singolo database in molti pezzi più piccoli, quindi eseguendo l'algoritmo normale su ciascuna sottosezione. Mi piace questa citazione. "L'algoritmo descritto è imbarazzantemente parallelo". Nel loro caso hanno usato CUDA e hanno dovuto fare un sacco di ottimizzazione della memoria, ma il principio dovrebbe ancora applicarsi alle macchine multi-core.

SEGUE semi-PSEUDOCODE. Userò gli elenchi per gli hash byte in entrata [], spero che sia o.k.

originale, 1 metodo del nucleo

originalProcess(List<byte[]> list1, List<byte[]> list2) { 
    HashSet<byte[]> bigHugeHashOfList1 = new HashSet<byte[]>(); 
    bigHugeHashOfList1.addAll(list1); 
    for (byte[] hash : list2) 
     if (bigHugeHashOfList1.contains(hash) 
     // do something 
} 

Nuovo metodo. Utilizza lo stesso identico metodo di processo (in seguito). No DAWGS o TRIES qui ...

preprocess(List<byte[]> list1, List<byte[]> list2) { 
    List<byte[]>[] splitLists = new ArrayList<byte[]>[8]; 
    for (int i=0; i<8; i++) 
     splitLists[i] = new ArrayList<byte[]>(); 
    for (byte[] hash : list1) { 
     int idx = hash[0]&7; // I'm taking the 3 low order bits, YMMV 
     splitLists[idx].add(hash); 
     // a minor speedup would be to create the HashSet here instead of in originalProcess() 
    } 

    // now, using your favorite parallel/concurrency technique, 
    // do the equivalent of 
    for (int i=0; i<8; i++) 
     originalProcess(splitLists[i], list2); 
}  
+1

Il tuo approccio è possibile e più semplice, ma meno efficiente. Verificare se un elemento si trova all'interno di un array di lunghezza n costa fino a O (n), perché è necessario eseguire un'iterazione attraverso l'array. HashMaps e Tries eseguono ricerche in O (1), che è molto più veloce. (Sidenote: Tries può normalmente avere un tempo di ricerca di O (m), dove m è la lunghezza della parola.In questo caso speciale tutte le parole hanno la stessa lunghezza (costante), quindi non ha alcun effetto sul big-O- notazione.) –

+1

È ancora possibile utilizzare una HashMap per i N piccoli problemi secondari. Proprio come la tua soluzione single-core originale. Questo è ciò che intendevo per "normale". Un vantaggio è che non devono essere concomitanti. – user949300

+1

È possibile suddividere gli 8 core prendendo i primi 3 bit dell'hash come discriminatore. Questo sarebbe un primo passo eccellente. – OldCurmudgeon