2012-01-02 11 views
6

Estoy tratando de implementar un ataque de colisión en hash (estoy visitando el curso 'criptografía'). Por lo tanto, tengo dos matrices de hashes (= byte-sequences byte[]) y quiero encontrar hashes que estén presentes en ambas matrices. Después de investigar un poco y pensarlo mucho, estoy seguro de que la mejor solución para una máquina de un solo núcleo sería HashSet (agregue todos los elementos del primer conjunto y verifique a través de contains si los elementos del segundo conjunto ya están presentes).¿Cómo encontrar byte idéntico [] - objetos en dos matrices al mismo tiempo?

Sin embargo, quiero implementar una solución concurrente, ya que tengo acceso a una máquina con 8 núcleos y 12 GB de RAM. La mejor solución que puedo pensar es ConcurrentHashSet, que podría crearse a través del Collections.newSetFromMap(new ConcurrentHashMap<A,B>()). Utilizando esta estructura de datos, pude agregar todos los elementos de la primera matriz en paralelo y, después de todos los elementos en los que se agregaron, puedo verificar concurrentemente a través de contains para hashes idénticos.

Así que mi pregunta es: ¿Conoces un algoritmo diseñado para este problema exacto? De lo contrario, ¿tiene experiencia en el uso de dicho ConcurrentHashSet con respecto a los problemas y la complejidad efectiva del tiempo de ejecución? ¿O puede recomendar otra estructura de datos preconstruida que podría ayudarme?

PD: Si alguien está interesado en los detalles: Planeo usar Skandium para paralelizar mi programa.

+0

¿Los arreglos ya están ordenados? Si es así, una función de fusión de un solo paso encontrará duplicados. De lo contrario, podría ordenar array1 y array2 en paralelo y fusionar los resultados. – Ingo

+1

Por hash de bytes ¿quiere decir que todos los hash están en el intervalo 0-255? – Tudor

+0

Quise decir byte-secuencias, es decir 'byte []'. Son el resultado de una función hash como SHA o MD5. No, las matrices no están ordenadas. Ordenarlas y fusionarlas necesitaría O (n log n) para la clasificación y O (n + m) para la fusión. Esperaba una mayor eficiencia. –

Respuesta

5

Creo que sería una completa pérdida de tiempo utilizar cualquier forma de HashMap. Supongo que estás calculando hashes de múltiples bytes de varios datos, estos ya son hash es, no hay necesidad de realizar más hashing en ellos.

Aunque no lo diga, supongo que sus hashes son byte secuencias. Claramente, un trie o un dawg sería ideal para almacenarlos.

Sugiero que implemente un trie/dawg y lo use para almacenar todos los valores hash en la primera matriz. A continuación, puede utilizar toda su potencia de cálculo en paralelo para buscar cada elemento en su segunda matriz en este trie. No se requieren cerraduras.

Agregado

He aquí una sencilla aplicación Dawg Toqué juntos. Parece funcionar.

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

Agregado

Esto podría ser un buen comienzo en una versión concurrente sin bloqueo. Estas cosas son notoriamente difíciles de probar, así que no puedo garantizar que esto funcione, pero en mi opinión, sin duda debería hacerlo.

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í, tienes razón, me gustaría hash hashes que suena horrible. Pero no podía pensar en otra forma de usar estructuras de datos preconstruidas. Pensé en Tries también, pero tienen búsquedas en O (log n) en lugar de O (1) tiene un HashSet - o estoy equivocado al respecto? Además, si puedo anular el método hash del HashSet, podría poner mis datos directamente en eso, evitando el hashing de hashes. (Pero no pude ver cómo hacerlo en JavaDoc de HashSet.) –

+1

@FlorianPilz el tiempo de acceso (peor caso) de un Trie es de hecho O (log n), donde n = número de "caracteres" en su " palabra". Pero dado que los hashes tienen todos la misma longitud, esto es irrelevante, ya que n es siempre el mismo. Además, tenga en cuenta que O (1) puede tomar más tiempo que incluso O (e^n) para n pequeña y es solo la asíntota que forma parte de la notación O(). –

+1

@nd Gracias por tu comentario. Si te entiendo bien, el Trie tendría O (1) el mejor de los casos y el peor de los casos, ya que la duración de mis palabras es constante. Después de leer un poco más, comprendo que HashMap y Trie son comparables en velocidad (especialmente en este escenario), así que Paul tiene razón: A Trie sería mejor, ya que no pierdo velocidad, pero ahorro memoria y tengo un peor caso. complejidad de tiempo de ejecución. Si lo hice bien, esta solución produce una complejidad garantizada de tiempo de ejecución O (2 * n), si las matrices contienen n hashes. ¿Correcto? –

0

Un enfoque más sencillo sería simplemente dividir la primera matriz en N partes iguales (o casi iguales) (con 8 núcleos, n = 8 parece razonable). Luego resuelva el programa de la manera "normal", observando si hay hashes en la segunda matriz en las N sub-primeras-matrices más pequeñas. Esto se puede hacer en paralelo.

Dicho esto, nunca antes había escuchado sobre tries/dawgs y encontré la discusión principal fascinante e informativa.(Principalmente trabajo con números, no palabras)

Esto supone que los valores hash de byte [] son ​​de cierta longitud finita y corta para que realmente pueda dividir el archivo original para procesar en paralelo. Es ese el caso?

EDITAR AÑADIDO

Para un ejemplo de esta idea, ver GPU Gems gráficos, editado por Wen-Mei W. Hwu, capítulo 11, un artículo de Ligowski, Rudnicki, Liu y Schmidt. Paralelamente una búsqueda masiva de base de datos de secuencias de proteínas dividiendo la enorme base de datos individual en muchas piezas más pequeñas, luego ejecuta el algoritmo normal en cada subclase. Me gusta esta cita "El algoritmo descrito es embarazosamente paralelo". En su caso, usaron CUDA y tuvieron que hacer mucha optimización de la memoria, pero el principio todavía debería aplicarse a las máquinas multi-core.

semi-PSEUDOCODE ESTA SIGUIENDO Usaré las listas para los hashes entrantes [], espero que sean o.k.

original, 1 Método básico

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 
} 

Nuevo método. Utiliza el mismo método de proceso (más adelante). No hay DAWGS o TRIES aquí ...

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

Su enfoque es posible y más simple, pero menos eficiente. Probar si un elemento está dentro de un Arrays de longitud n cuesta hasta O (n), porque tiene que iterar a través de la matriz. HashMaps y Tries realizan búsquedas en O (1), que es mucho más rápido. (Nota: Tries normalmente puede tener un tiempo de búsqueda de O (m), donde m es la longitud de la palabra. En este caso especial, todas las palabras tienen la misma longitud (constante), por lo tanto, no tiene ningún efecto en el gran O-O- notación). –

+1

Aún puede utilizar un HashMap para los N-sub problemas menores. Al igual que su solución original de un solo núcleo. Eso es lo que quise decir con la forma "normal". Una ventaja es que no necesitan ser concurrentes. – user949300

+1

Puede dividir en 8 núcleos tomando los primeros 3 bits del hash como discriminador. Este sería un excelente primer paso. – OldCurmudgeon

Cuestiones relacionadas