2011-08-23 14 views
5

Estoy tratando de ejecutar un cálculo simple (llama a Math.random() 10000000 veces). Sorpresivamente, ejecutarlo en un método simple funciona mucho más rápido que usar ExecutorService.ExecutorService rendimiento lento multi hilo

He leído en otro hilo ExecutorService's surprising performance break-even point --- rules of thumb? y tratado de seguir la respuesta mediante la ejecución de los Callable utilizando lotes, pero el rendimiento sigue siendo mala

¿Cómo puedo mejorar el rendimiento basado en mi código actual?

import java.util.*; 
import java.util.concurrent.*; 

public class MainTest { 
    public static void main(String[]args) throws Exception { 
     new MainTest().start();; 
    } 

    final List<Worker> workermulti = new ArrayList<Worker>(); 
    final List<Worker> workersingle = new ArrayList<Worker>(); 
    final int count=10000000; 

    public void start() throws Exception { 
     int n=2; 

     workersingle.add(new Worker(1)); 
     for (int i=0;i<n;i++) { 
      // worker will only do count/n job 
      workermulti.add(new Worker(n)); 
     } 

     ExecutorService serviceSingle = Executors.newSingleThreadExecutor(); 
     ExecutorService serviceMulti = Executors.newFixedThreadPool(n); 
     long s,e; 
     int tests=10; 
     List<Long> simple = new ArrayList<Long>(); 
     List<Long> single = new ArrayList<Long>(); 
     List<Long> multi = new ArrayList<Long>(); 

     for (int i=0;i<tests;i++) { 
      // simple 
      s = System.currentTimeMillis(); 
      simple(); 
      e = System.currentTimeMillis(); 
      simple.add(e-s); 

      // single thread 
      s = System.currentTimeMillis(); 
       serviceSingle.invokeAll(workersingle); // single thread 
      e = System.currentTimeMillis(); 
      single.add(e-s); 

      // multi thread 
      s = System.currentTimeMillis(); 
       serviceMulti.invokeAll(workermulti); 
      e = System.currentTimeMillis(); 
      multi.add(e-s); 
     } 
     long avgSimple=sum(simple)/tests; 
     long avgSingle=sum(single)/tests; 
     long avgMulti=sum(multi)/tests; 
     System.out.println("Average simple: "+avgSimple+" ms"); 
     System.out.println("Average single thread: "+avgSingle+" ms"); 
     System.out.println("Average multi thread: "+avgMulti+" ms"); 

     serviceSingle.shutdown(); 
     serviceMulti.shutdown(); 
    } 

    long sum(List<Long> list) { 
     long sum=0; 
     for (long l : list) { 
      sum+=l; 
     } 
     return sum; 
    } 

    private void simple() { 
     for (int i=0;i<count;i++){ 
      Math.random(); 
     } 
    } 

    class Worker implements Callable<Void> { 
     int n; 

     public Worker(int n) { 
      this.n=n; 
     } 

     @Override 
     public Void call() throws Exception { 
      // divide count with n to perform batch execution 
      for (int i=0;i<(count/n);i++) { 
       Math.random(); 
      } 
      return null; 
     } 
    } 
} 

La salida para este código

Average simple: 920 ms 
Average single thread: 1034 ms 
Average multi thread: 1393 ms 

EDIT: rendimiento sufrir debido a Math.random() que es un método sincronizado .. después de cambiar Math.random() con nuevo objeto aleatoria para cada hilo , el rendimiento mejoró

la salida para el nuevo código (después de reemplazar Math.random() con Random para cada hilo)

Average simple: 928 ms 
Average single thread: 1046 ms 
Average multi thread: 642 ms 

Respuesta

12

Math.random() está sincronizado. Tipo de todo el punto de sincronización es ralentizar las cosas para que no colisionen. Utilice algo que no esté sincronizado y/o dé a cada hilo su propio objeto para trabajar, como un nuevo Random.

+0

¡Ah, tienes razón! No me di cuenta de que Math.random() está sincronizado. Una vez que puse un nuevo objeto aleatorio para cada trabajador, el rendimiento mejoró enormemente – GantengX

+0

Solo una pregunta rápida, si traté de compartir el objeto Random, el rendimiento aún sufre. ¿Sabes por qué es esto así? Random.nextDouble no está sincronizado y llama a Random.next (int) que a su vez llama a AtomicLong.compareAndSet ..No veo por qué esto afectaría el rendimiento. – GantengX

+4

Supongo que porque acabas de volver a tener múltiples hilos contender por el mismo recurso de nuevo: el AtomicLong en este caso. Solo un hilo puede actualizar su valor a la vez, y se actualiza dos veces para cada llamada a nextDouble(). –

3

Haría bien en leer el contenido del otro hilo. Hay muchos buenos consejos allí.

Quizás el problema más importante con su punto de referencia es que según el contrato Math.random(), "Este método está sincronizado correctamente para permitir el uso correcto por más de un hilo. Sin embargo, si muchos hilos necesitan generar números pseudoaleatorios a gran velocidad, puede reducir la contención para que cada subproceso tenga su propio generador de números pseudoaleatorios "

Lea esto como: el método está sincronizado, por lo que solo es probable que un subproceso pueda usarlo al mismo tiempo hora. Así que haces un montón de sobrecarga para distribuir las tareas, solo para forzarlas nuevamente a ejecutar en serie.

1

Cuando utiliza varios subprocesos, debe tener en cuenta la sobrecarga de utilizar subprocesos adicionales. También debe determinar si su algoritmo tiene un trabajo que puede realizarse en paralelo o no. Por lo tanto, debe tener un trabajo que pueda ejecutarse simultáneamente, que sea lo suficientemente grande como para exceder la sobrecarga de usar varios hilos.

En este caso, la solución más simple es usar una Aleatoriedad separada en cada subproceso. El problema que tienes es que, como micro-benchmark, tu loop en realidad no hace nada y el JIT es muy bueno descartando código que no hace nada. Una solución para esto es sumar los resultados aleatorios y devolverlos desde call() ya que esto es generalmente suficiente para evitar que el JIT descarte el código.

Por último, si desea sumar muchos números, no necesita guardarlos y resumirlos más tarde. Puedes sumarlos sobre la marcha.

Cuestiones relacionadas