2010-11-24 7 views
9

Escribí algunos códigos Java para aprender más sobre el marco Ejecutor.¿Por qué este código no muestra ninguna ganancia de rendimiento significativa cuando uso múltiples hilos en una máquina de cuatro núcleos?

Específicamente, escribí código para verificar la Collatz Hypothesis - esto dice que si iterativa aplicar la siguiente función para cualquier número entero, se llega a 1 con el tiempo:

f (n) = ((n% 2) = = 0)? n/2: 3 * n + 1

CH aún no se ha probado, y pensé que sería una buena forma de aprender sobre el ejecutor. Cada hilo tiene asignado un rango [l, u] de enteros para verificar.

Específicamente, mi programa tiene 3 argumentos: N (el número al que quiero verificar CH), RANGESIZE (la longitud del intervalo que un subproceso debe procesar) y NTHREAD, el tamaño del subproceso de subprocesos.

Mi código funciona bien, pero vi una aceleración mucho menor que la esperada, del orden del 30% cuando pasé de 1 a 4 hilos.

Mi lógica era que el cálculo está completamente vinculado a la CPU, y cada subtarea (verificando CH para un rango de tamaño fijo) toma más o menos el mismo tiempo.

¿Alguien tiene ideas de por qué no veo un incremento de 3 a 4 veces en la velocidad?

Si puede informar sus tiempos de ejecución a medida que aumenta el número de subprocesos (junto con la máquina, JVM y SO) que también sería genial.

Específicos

Runtimes:

java -d64 -server -cp. Collatz 10000000 1000000 4 => 4 hilos, lleva 28412 milisegundos

java -d64 -server -cp. Collatz 10000000 1000000 1 => 1 hilo, toma 38286 milisegundos

Procesador:

Q6600 Quadcore Intel a 2,4 GHz, 4 GB. La máquina está descargada.

Java:

java version "1.6.0_15" Java (TM) SE Runtime Environment (build 1.6.0_15-b03) Java HotSpot (TM) de 64 bits del servidor VM (build 14.1-b02, de modo mixto)

OS:

Linux quad0 2.6.26-2-amd64 # 1 SMP Martes 9 de marzo 22:29:32 UTC 2010 x86_64 GNU/Linux

Código: (No puedo obtener el código para publicar, creo que es demasiado largo para los requisitos de SO, la fuente está disponible en Google Docs

import java.math.BigInteger; 
import java.util.Date; 
import java.util.List; 
import java.util.ArrayList; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 

class MyRunnable implements Runnable { 
    public int lower; 
    public int upper; 

    MyRunnable(int lower, int upper) { 
    this.lower = lower; 
    this.upper = upper; 
    } 

    @Override 
    public void run() { 
    for (int i = lower ; i <= upper; i++) { 
     Collatz.check(i); 
    } 
    System.out.println("(" + lower + "," + upper + ")"); 
    } 
} 


public class Collatz { 

    public static boolean check(BigInteger X) { 
    if (X.equals(BigInteger.ONE)) { 
     return true; 
    } else if (X.getLowestSetBit() == 1) { 
     // odd 
     BigInteger Y = (new BigInteger("3")).multiply(X).add(BigInteger.ONE); 
     return check(Y); 
    } else { 
     BigInteger Z = X.shiftRight(1); // fast divide by 2 
     return check(Z); 
    } 
    } 

    public static boolean check(int x) { 
    BigInteger X = new BigInteger(new Integer(x).toString()); 
    return check(X); 
    } 

    static int N = 10000000; 
    static int RANGESIZE = 1000000; 
    static int NTHREADS = 4; 

    static void parseArgs(String [] args) { 

    if (args.length >= 1) { 
     N = Integer.parseInt(args[0]); 
    } 
    if (args.length >= 2) { 
     RANGESIZE = Integer.parseInt(args[1]); 
    } 
    if (args.length >= 3) { 
     NTHREADS = Integer.parseInt(args[2]); 
    } 
    } 

    public static void maintest(String [] args) { 
    System.out.println("check(1): " + check(1)); 
    System.out.println("check(3): " + check(3)); 
    System.out.println("check(8): " + check(8)); 
    parseArgs(args); 
    } 

    public static void main(String [] args) { 
    long lDateTime = new Date().getTime(); 
    parseArgs(args); 
    List<Thread> threads = new ArrayList<Thread>(); 
    ExecutorService executor = Executors.newFixedThreadPool(NTHREADS); 
    for(int i = 0 ; i < (N/RANGESIZE); i++) { 
     Runnable worker = new MyRunnable(i*RANGESIZE+1, (i+1)*RANGESIZE); 
     executor.execute(worker); 
    } 
    executor.shutdown(); 
    while (!executor.isTerminated()) { 
    } 
    System.out.println("Finished all threads"); 
    long fDateTime = new Date().getTime(); 
    System.out.println("time in milliseconds for checking to " + N + " is " + 
          (fDateTime - lDateTime) + 
          " (" + N/(fDateTime - lDateTime) + " per ms)"); 
    } 
} 
+0

Su código de enlace falló para mí. –

+0

Creo que necesita cambiar la configuración en su documento de Google. Obtengo "Google Docs: página no disponible". –

+0

Pude obtener el código. Lo agregué al final de la pregunta. –

Respuesta

11

de espera ocupado puede ser un problema:

while (!executor.isTerminated()) { 
} 

Usted puede utilizar awaitTermination() lugar:

while (!executor.awaitTermination(1, TimeUnit.SECONDS)) {} 
+0

Buena idea. Intenta eliminarlo BTW shutdown() funciona con gracia. No mata los hilos. Simplemente les dice que terminen cuando terminen, por lo que no necesita el bucle ocupado en absoluto. – AlexR

+2

@AlexR: No completamente así. 'shutdown()' no bloquea, no espera a que los hilos terminen. Se necesita algún tipo de espera para realizar el tiempo. – axtavt

+0

+1, ejecuté esto para kicks y en mi máquina de dos núcleos, lo que elimina el aumento de la espera en el rendimiento ocupado en un 75% con la ejecución de subprocesos ejecutor. – Affe

2

Usted está usando BigInteger. Consume mucho espacio de registro. Lo que probablemente tenga en el nivel del compilador es el desbordamiento de registros que hace que su proceso esté ligado a la memoria.

También tenga en cuenta que cuando está cronometrando los resultados, no está teniendo en cuenta el tiempo extra que toma la JVM para asignar subprocesos y trabajar con el grupo de subprocesos.

También podría tener conflictos de memoria cuando utilice cadenas constantes. Todas las cadenas se almacenan en un grupo compartido de cadenas y puede convertirse en un cuello de botella, a menos que Java sea realmente inteligente al respecto.

En general, no aconsejaría usar Java para este tipo de cosas. Usar pthreads sería una mejor manera de hacerlo.

+0

Creo que te perdiste la segunda línea: "Escribí algunos códigos Java para aprender más sobre el framework Executor." ¿Java sería perfecto para eso? El código no fue diseñado para ser eficiente;) – Ishtar

-1

Puede intentar usar la función de envío y luego ver los futuros que están regresando comprobándolos para ver si el hilo ha finalizado.

Terminate no regresa hasta que se apaga.

Envío futuro (tarea ejecutable) Envía una tarea ejecutable para su ejecución y devuelve un futuro que representa esa tarea.

isTerminated() Devuelve verdadero si se han completado todas las tareas después del apagado.

Prueba esto ...

public static void main(String[] args) { 
    long lDateTime = new Date().getTime(); 
    parseArgs(args); 
    List<Thread> threads = new ArrayList<Thread>(); 
    List<Future> futures = new ArrayList<Future>(); 

    ExecutorService executor = Executors.newFixedThreadPool(NTHREADS); 
    for (int i = 0; i < (N/RANGESIZE); i++) { 
     Runnable worker = new MyRunnable(i * RANGESIZE + 1, (i + 1) * RANGESIZE); 
     futures.add(executor.submit(worker)); 
    } 
    boolean done = false; 
    while (!done) { 
     for(Future future : futures) { 
      done = true; 
      if(!future.isDone()) { 
       done = false; 
       break; 
      } 
     } 
     try { 
      Thread.sleep(100); 
     } catch (InterruptedException e) { 
      e.printStackTrace(); 
     } 
    } 

    System.out.println("Finished all threads"); 
    long fDateTime = new Date().getTime(); 
    System.out.println("time in milliseconds for checking to " + N + " is " + 
      (fDateTime - lDateTime) + 
      " (" + N/(fDateTime - lDateTime) + " per ms)"); 
    System.exit(0); 
} 
+2

Código con thread.sleep() tiene un mal olor, incluso en una demostración .... – bwawok

+0

si desea utilizar futuros para la finalización, eche un vistazo al CompletionService/ExecutorCompletionService . –

2

Como @axtavt respondió, ocupado de espera puede ser un problema. Primero debe solucionarlo, ya que es parte de la respuesta, pero no toda. No parece ser de ayuda en su caso (en Q6600), porque parece estar embotellado en 2 núcleos por alguna razón, por lo que hay otro disponible para el bucle ocupado y por lo tanto no hay desaceleración aparente, pero en mi Core i5 acelera notablemente la versión de 4 hilos.

Sospecho que, en el caso del Q6600, su aplicación particular está limitada por la cantidad de caché compartida disponible o por otra cosa específica para la arquitectura de esa CPU. El Q6600 tiene dos cachés L2 de 4MB, lo que significa que los CPU los están compartiendo, y no tiene caché L3. En mi core i5, cada CPU tiene un caché L2 dedicado (256K, luego hay un caché L3 compartido más grande de 8MB. 256K más de caché por CPU pueden marcar la diferencia ... de lo contrario, otra arquitectura lo hace.

Aquí es una comparación de un Q6600 ejecutando su Collatz.java, y un Core i5 750.

En mi PC de trabajo, que también es un Q6600 @ 2.4GHz como el suyo, pero con 6 GB de RAM, Windows 7 de 64 bits y JDK 1.6.0_21 * (64-bit), aquí están algunos resultados básicos:

  • 10000000 500000 1 (avg de tres carreras): 36982 ms
  • 10000000 500 000 4 (promedio de tres carreras): 21252 ms

Más rápido, sin duda, pero no se completa en un cuarto del tiempo como era de esperar, ni siquiera la mitad ... (aunque es aproximadamente un poco más de la mitad, más sobre eso en un momento). Tenga en cuenta que en mi caso reduje a la mitad el tamaño de las unidades de trabajo, y tengo un montón máximo predeterminado de 1500 m.

en casa en mi i5 Core 750 (4 núcleos no hyperthreading), 4 GB de RAM, Windows 7 de 64 bits, JDK 1.6.0_22 (64-bit):

  • 10000000 500000 1 (promedio de 3 carreras) 32677 ms
  • 10000000 500000 4 (promedio de 3 carreras) 8825 ms
  • 10000000 500000 4 (promedio de 3 carreras) 11475 ms (sin la solución de espera ocupado, por referencia)

la 4 la versión de hilos tarda el 27% del tiempo en que la versión de 1 hilo toma wh es el lazo busy-wait se elimina. Mucho mejor. Es evidente que el código se puede hacer un uso eficiente de 4 núcleos ...

  • NOTA: Java 1.6.0_18 y valores de almacenamiento dinámico predeterminado más tarde han modificado - por lo que mi tamaño de la pila por defecto es casi 1500m en mi PC de trabajo, y alrededor de 1000 metros sobre el la computadora de mi casa.

Es posible que desee aumentar su montón predeterminado, por si acaso se produce la recolección de basura y ralentizar un poco la versión de 4 hilos. Podría ayudar, quizás no.

Al menos en su ejemplo, existe la posibilidad de que su tamaño de unidad de trabajo grande desvíe ligeramente sus resultados ... reducirlo a la mitad puede ayudarlo a acercarse a al menos 2 veces la velocidad ya que 4 hilos se mantendrán ocupados durante una porción más larga del tiempo. No creo que el Q6600 sea mucho mejor en esta tarea en particular ... ya sea un caché o alguna otra cosa arquitectónica inherente.

En todos los casos, simplemente estoy ejecutando "java Collatz 10000000 500000 X", donde x = # de subprocesos indicados.

Los únicos cambios que realicé en su archivo java fueron hacer una impresión en una impresión, por lo que hubo menos saltos de línea para mis ejecuciones con 500000 por unidad de trabajo para poder ver más resultados en mi consola a la vez, y Me deshice del bucle de espera ocupado, que importa en el i5 750 pero no hizo la diferencia en el Q6600.

Cuestiones relacionadas