2011-08-23 16 views
5

Sto tentando di eseguire un semplice calcolo (chiama Math.random() 10000000 volte). Sorprendentemente, l'esecuzione nel metodo semplice si esegue molto più rapidamente rispetto all'utilizzo di ExecutorService.Prestazioni multi thread lente ExecutorService

Ho letto un altro thread a ExecutorService's surprising performance break-even point --- rules of thumb? e ha cercato di seguire la risposta eseguendo i Callable utilizzando lotti, ma la prestazione è ancora male

Come si migliorano le prestazioni in base alla mia codice corrente?

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

L'uscita di questo codice

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

EDIT: prestazioni soffrire a causa di Math.random() essendo un metodo sincronizzato .. dopo aver cambiato Math.random() con nuovo oggetto Random per ogni filo , le prestazioni migliorate

l'uscita per il nuovo codice (dopo la sostituzione Math.random() con casuale per ogni filo)

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

risposta

12

Math.random() è sincronizzato. Il tipo di punto sincronizzato è quello di rallentare le cose in modo che non entrino in collisione. Usa qualcosa che non è sincronizzato e/o dai a ciascun thread il suo oggetto con cui lavorare, come un nuovo Random.

+0

Ah hai ragione! Non avevo capito che Math.random() è sincronizzato .. Una volta inserito un nuovo oggetto Random per ciascun lavoratore, le prestazioni sono migliorate notevolmente – GantengX

+0

Solo una domanda veloce, se ho provato a condividere l'oggetto Random, le prestazioni continuano a risentirne. Sai perché è così? Random.nextDouble non è sincronizzato e chiama Random.next (int) che a sua volta chiama AtomicLong.compareAndSet ..Non vedo perché questo influirebbe sulle prestazioni – GantengX

+4

Immagino perché si sta solo tornando ad avere più thread contendere per la stessa risorsa di nuovo: il AtomicLong in questo caso. Solo un thread può aggiornare il suo valore alla volta e viene aggiornato due volte per ogni chiamata a nextDouble(). –

3

Farebbe bene a leggere il contenuto dell'altro thread. Ci sono molti buoni consigli là dentro.

Forse il problema più significativo del benchmark è che, in base al contratto Math.random(), "Questo metodo è correttamente sincronizzato per consentire l'uso corretto di più thread, tuttavia se molti thread devono generare numeri pseudocasuali ad un ottimo ritmo, può ridurre la contesa per ogni thread di avere il proprio generatore di numeri pseudocasuali "

Leggere questo come: il metodo è sincronizzato, quindi è probabile che solo un thread sia in grado di usarlo utilmente allo stesso tempo. Quindi fai un po 'di overhead per distribuire i compiti, solo per costringerli di nuovo a correre in serie.

1

Quando si utilizzano più thread, è necessario essere consapevoli del sovraccarico dell'utilizzo di thread aggiuntivi. È inoltre necessario determinare se il proprio algoritmo ha un lavoro che può essere preformato in parallelo o meno. Quindi è necessario disporre di un lavoro che può essere eseguito contemporaneamente che è abbastanza grande da superare il sovraccarico dell'utilizzo di più thread.

In questo caso, la soluzione più semplice consiste nell'utilizzare un Random separato in ogni thread. Il problema che hai è che come un micro-benchmark, il tuo loop in realtà non fa nulla e il JIT è molto bravo a scartare il codice che non fa nulla. Una soluzione per questo è sommare i risultati casuali e restituirli dallo call() poiché questo è di solito sufficiente per impedire al JIT di scartare il codice.

Infine, se si desidera sommare molti numeri, non è necessario salvarli e sommarli in un secondo momento. Puoi sommarli mentre vai.

Problemi correlati