2010-05-21 10 views
5

Existen ciertos algoritmos cuyo tiempo de ejecución puede disminuir significativamente cuando se divide una tarea y se hace cada parte en paralelo. Uno de estos algoritmos es el tipo de fusión, donde una lista se divide en partes infinitesimalmente más pequeñas y luego se recombinan en un orden ordenado. Decidí hacer un experimento para probar si podía o no aumentar la velocidad de este tipo mediante el uso de varios hilos. Estoy ejecutando las siguientes funciones en Java en un Dell de cuatro núcleos con Windows Vista.¿Por qué mi algoritmo de ordenación multihilo no es más rápido que mi mergesort de un solo hilo?

Una función (el caso de control) es simplemente recursiva:

// x is an array of N elements in random order 
public int[] mergeSort(int[] x) { 
    if (x.length == 1) 
     return x; 

    // Dividing the array in half 
    int[] a = new int[x.length/2]; 
    int[] b = new int[x.length/2+((x.length%2 == 1)?1:0)]; 
    for(int i = 0; i < x.length/2; i++) 
     a[i] = x[i]; 
    for(int i = 0; i < x.length/2+((x.length%2 == 1)?1:0); i++) 
     b[i] = x[i+x.length/2]; 

    // Sending them off to continue being divided 
    mergeSort(a); 
    mergeSort(b); 

    // Recombining the two arrays 
    int ia = 0, ib = 0, i = 0; 
    while(ia != a.length || ib != b.length) { 
     if (ia == a.length) { 
      x[i] = b[ib]; 
      ib++; 
     } 
     else if (ib == b.length) { 
      x[i] = a[ia]; 
      ia++; 
     } 
     else if (a[ia] < b[ib]) { 
      x[i] = a[ia]; 
      ia++; 
     } 
     else { 
      x[i] = b[ib]; 
      ib++; 
     } 
     i++; 
    } 

    return x; 
} 

El otro está en la función 'run' de una clase que se extiende hilo de rosca, y de forma recursiva crea dos nuevos temas cada vez que se llama:

public class Merger extends Thread 
{ 
    int[] x; 
    boolean finished; 

    public Merger(int[] x) 
    { 
     this.x = x; 
    } 

    public void run() 
    { 
     if (x.length == 1) { 
      finished = true; 
      return; 
     } 

     // Divide the array in half 
     int[] a = new int[x.length/2]; 
     int[] b = new int[x.length/2+((x.length%2 == 1)?1:0)]; 
     for(int i = 0; i < x.length/2; i++) 
      a[i] = x[i]; 
     for(int i = 0; i < x.length/2+((x.length%2 == 1)?1:0); i++) 
      b[i] = x[i+x.length/2]; 

     // Begin two threads to continue to divide the array 
     Merger ma = new Merger(a); 
     ma.run(); 
     Merger mb = new Merger(b); 
     mb.run(); 

     // Wait for the two other threads to finish 
     while(!ma.finished || !mb.finished) ; 

     // Recombine the two arrays 
     int ia = 0, ib = 0, i = 0; 
     while(ia != a.length || ib != b.length) { 
      if (ia == a.length) { 
       x[i] = b[ib]; 
       ib++; 
      } 
      else if (ib == b.length) { 
       x[i] = a[ia]; 
       ia++; 
      } 
      else if (a[ia] < b[ib]) { 
       x[i] = a[ia]; 
       ia++; 
      } 
      else { 
       x[i] = b[ib]; 
       ib++; 
      } 
      i++; 
     } 

     finished = true; 
    } 
} 

Resulta que la función que no usa multithreading en realidad funciona más rápido. ¿Por qué? ¿El sistema operativo y la máquina virtual Java no se "comunican" con la suficiente eficacia como para colocar los diferentes hilos en diferentes núcleos? ¿O me estoy perdiendo algo obvio?

+0

Cuando tiene un hilo, tiene todos sus datos en el caché. Si divide los datos entre subprocesos, algunos de los datos deben copiarse en la memoria caché del segundo subproceso y luego copiarse nuevamente para la fusión final. Si el costo de la comparación es pequeño, el costo de la copia podría ser mayor. Es posible que descubras que cuantos más hilos uses, mayor será la sobrecarga. –

+4

No veo que llames a Thread.start() en ningún lado. Su fusión se ve rota ya que solo está llamando al método run() todo en un hilo. – Justin

+0

Los programadores realmente necesitan salir de la mentalidad de "un solo hilo": es como si los años ochenta llamaran y quisieran recuperar su CPU 80386 de un solo núcleo. Nos estamos moviendo a un mundo con múltiples núcleos y las mejoras más grandes que se producirán en el futuro se deberán a la ampliación horizontal de varios núcleos. Las personas que se nieguen a aprender los beneficios de múltiples subprocesos producirán API/software de bajo rendimiento. El OP puede verificar mi respuesta: los algoritmos de subprocesos correctos (incluidos los de ordenación) tienen ** propios ** algos de subprocesos múltiples. Superar esto, nos estamos moviendo a un mundo de múltiples núcleos. – SyntaxT3rr0r

Respuesta

12

El problema no es multi-threading: He escrito un QuickSort de múltiples subprocesos correctamente en Java y es posee el ordenamiento Java predeterminado. Hice esto después de ser testigo de un gigantesco conjunto de datos en proceso y tenía solo un núcleo de una máquina de 16 núcleos funcionando.

Uno de su problema (uno grande) es que estás ocupado bucle:

// Wait for the two other threads to finish 
while(!ma.finished || !mb.finished) ; 

Ésta es una enorme no-no: se llama bucle ocupado y ya está destruyendo las perforaciones .

(Otra cuestión es que su código no está generando ningún nuevos temas, como ya se ha señalado a usted)

Es necesario utilizar otra forma de sincronizar: un ejemplo sería el uso de una CountDownLatch.

Otra cosa: no hay necesidad de engendrar dos hilos nuevos al dividir la carga de trabajo: engendrar solo un hilo nuevo, y hacer la otra mitad en el hilo actual.

Además, probablemente no desee crear más subprocesos que núcleos disponibles.

Consulte mi pregunta aquí (solicitando una buena fuente abierta de multiproceso mergesort/quicksort/whatever). El que estoy usando es de propiedad, no puedo pegarlo.

Multithreaded quicksort or mergesort

no he implementado por fusión, pero QuickSort y te puedo decir que no hay un conjunto de copia pasando.

Lo que hago es la siguiente:

  • recoger un pivote
  • valores de cambio, según sea necesario
  • hemos llegado al límite de la rosca? (Dependiendo del número de núcleos)
    • sí: Ordenar primera parte en este hilo
    • no: generar un nuevo subproceso
  • especie segunda parte en la hebra actual
  • de espera para la primera parte de terminar si aún no está hecho (usando un CountDownLatch).

El código liberando un nuevo hilo y la creación de la CountDownLatch puede tener este aspecto:

  final CountDownLatch cdl = new CountDownLatch(1); 
      final Thread t = new Thread(new Runnable() { 
       public void run() { 
        quicksort(a, i+1, r); 
        cdl.countDown(); 
       } 
      } }; 

La ventaja de utilizar las instalaciones de sincronización como el CountDownLatch es que es muy eficiente y que su tiempo no perder tratar con idiosincrasias de sincronización Java de bajo nivel.

En su caso, el "split" puede tener este aspecto (no probado, que es sólo para dar una idea):

if (threads.getAndIncrement() < 4) { 
    final CountDownLatch innerLatch = new CountDownLatch(1); 
    final Thread t = new Merger(innerLatch, b); 
    t.start(); 
    mergeSort(a); 
    while (innerLatch.getCount() > 0) { 
     try { 
      innerLatch.await(1000, TimeUnit.SECONDS); 
     } catch (InterruptedException e) { 
      // Up to you to decide what to do here 
     } 
    } 
} else { 
    mergeSort(a); 
    mergeSort(b); 
} 

(no se olvide de "cuenta atrás" el pestillo cuando cada uno se fusionan está hecho)

Donde debe reemplazar el número de hilos (hasta 4 aquí) por la cantidad de núcleos disponibles. Puede usar lo siguiente (una vez, digamos, para inicializar alguna variable estática al comienzo de su programa: es poco probable que cambie la cantidad de núcleos [a menos que esté en una máquina que permita hotswapping de CPU como lo permiten algunos sistemas de Sun]):

Runtime.getRuntime().availableProcessors() 
+0

+1 para el concepto de bucle ocupado. – bragboy

+0

Vaya, tonto, debería haberlo reescrito en lugar de usar tu código: en el caso de que no generes un nuevo hilo, no tiene sentido dividirlo en 'a' y 'b' y luego hacer un mergeSort (a) y mergeSort (b) ... Simplemente fusioneSort directamente toda la matriz, antes de dividir. – SyntaxT3rr0r

+0

¿Por qué en la Tierra pondría la llamada a CDL.await() en un ciclo while? Además, usted es condicional (threads.getAndIncrement() <4) causaría que el 'recuento' de subprocesos creados aumentara independientemente de si generó uno. Del mismo modo, nunca se indica realmente cuándo se reduce ese conteo (aunque podría suponerse). –

1

El costo general de la sincronización puede ser comparativamente grande y evitar muchas optimizaciones.

Además, está creando demasiados hilos.

El otro está en la función 'run' de una clase que se extiende hilo, y recursivamente crea dos nuevos temas cada vez que se llama.

Sería mejor con un número fijo de hilos, sugestivamente 4 en un núcleo cuádruple. Esto podría realizarse con un grupo de subprocesos (tutorial) y el patrón sería una "bolsa de tareas". Pero quizás sería mejor aún, dividir inicialmente la tarea en cuatro tareas igualmente grandes y hacer una clasificación de "un único subproceso" en esas tareas. Esto luego utilizaría los cachés mucho mejor.


lugar de tener un "ocupado en bucle" en espera de los hilos para terminar (el robo de ciclos de CPU) que debe echar un vistazo a Thread.join().

+1

Si bien este es generalmente el caso con estos problemas, no hay sincronización en este ejemplo. –

+0

Woops. En ese caso, ¿debería haber algunas condiciones desagradables? – aioobe

+0

Oh, veo el bucle de espera ocupado ahora. – aioobe

0

¿Cuántos elementos de la matriz debe ordenar? Si hay muy pocos elementos, el tiempo de sincronización y la conmutación de la CPU sobre el tiempo que ahorrará para dividir el trabajo en paralelo

+0

N elementos están en la matriz, y N es un número muy grande (más de 1 millón). – Robz

+0

@Robz, estoy seguro de que se sorprendería al descubrir que la implementación de Sun de Arrays.sort tiene un valor de umbral mínimo para el que utiliza ordenación de inserción. El tamaño importa, punto. –

+0

Sí, lo sé :) – vodkhang

3

Como han dicho otros; Este código no va a funcionar porque no inicia nuevos hilos. Necesita llamar al método start() en lugar del método run() para crear nuevos hilos.También tiene errores de concurrencia: las comprobaciones de la variable finalizada no son seguras para subprocesos.

La programación simultánea puede ser bastante difícil si no comprende los conceptos básicos. Puede leer el libro Java Concurrency in Practice by Brian Goetz. Explica los conceptos básicos y explica los constructos (como Latch, etc.) para facilitar la creación de programas simultáneos.

Cuestiones relacionadas