2012-01-20 7 views
18

osservare la seguente definizione di una sottoclasse filo (l'intero file sorgente Java eseguibile è incluso alla fine della domanda per la vostra convenienza):Array allocazione e accesso sulla macchina virtuale e la memoria contesa Java

final class Worker extends Thread { 
    Foo[] array = new Foo[1024]; 
    int sz; 

    public Worker(int _sz) { 
     sz = _sz; 
    } 

    public void run() { 
     //Foo[] arr = new Foo[1024]; 
     Foo[] arr = array; 
     loop(arr); 
    } 

    public void loop(Foo[] arr) { 
     int i = 0; 
     int pos = 512; 
     Foo v = new Foo(); 
     while (i < sz) { 
      if (i % 2 == 0) { 
       arr[pos] = v; 
       pos += 1; 
      } else { 
       pos -= 1; 
       v = arr[pos]; 
      } 
      i++; 
     } 
    } 
} 

Spiegazione: Il programma avvia -Dpar tali thread e imposta sz di ogni thread su -Dsize/-Dpar, dove -Dsize e -Dpar vengono impostati tramite la riga di comando durante l'esecuzione del programma. Ogni oggetto thread ha un campo array che viene inizializzato con un nuovo array -element 1024. Il ragionamento è che vogliamo dividere una quantità uguale di lavoro tra un numero diverso di thread - ci aspettiamo che il programma si riduca.

Ogni thread viene quindi avviato e viene misurato il tempo necessario per completare tutti i thread. Facciamo più misurazioni per contrastare qualsiasi effetto correlato alla JIT, come mostrato di seguito. Ogni thread fa un ciclo. All'interno del ciclo, il thread legge un elemento nella posizione 512 nell'array in iterazioni uniformi e scrive lo stesso elemento su 512 in iterazioni dispari. Solo le variabili locali sono modificate diversamente.

Il programma completo è in basso.

Analisi:

Testato con -verbose:gc - non v'è alcun garbage collection che si verificano durante l'esecuzione di questo programma.

comando Esegui:

java -Xmx512m -Xms512m -server -Dsize=500000000 -Dpar=1 org.scalapool.bench.MultiStackJavaExperiment 7 

CASO 1: tempi di esecuzione per 1,2,4,8 discussioni, in questo ordine (7 ripetizioni):

>>> All running times: [2149, 2227, 1974, 1948, 1803, 2283, 1878] 
>>> All running times: [1140, 1124, 2022, 1141, 2028, 2004, 2136] 
>>> All running times: [867, 1022, 1457, 1342, 1436, 966, 1531] 
>>> All running times: [915, 864, 1245, 1243, 948, 790, 1007] 

Il mio pensiero era che la scala non lineare è dovuto a quanto sostiene la memoria . Per inciso, le iterazioni iniziali in realtà fanno meglio - questo potrebbe essere dovuto al fatto che in diverse iterazioni gli array sono allocati in diverse aree di memoria.

CASO 2: Successivamente, commento la linea Foo[] arr = array nel metodo del filo run e allocare una nuova matrice nel metodo run stessa: Foo[] arr = new Foo[1024]. Misure:

>>> All running times: [2053, 1966, 2089, 1937, 2046, 1909, 2011] 
>>> All running times: [1048, 1178, 1100, 1194, 1367, 1271, 1207] 
>>> All running times: [578, 508, 589, 571, 617, 643, 645] 
>>> All running times: [330, 299, 300, 322, 331, 324, 575] 

Questa volta, tutto scala in modo uniforme come previsto. Non avrei immaginato che il luogo in cui era stato assegnato l'array avesse un ruolo qualsiasi, ma ovviamente lo fa in qualche modo. Il mio pensiero era che gli array fossero stati precedentemente assegnati così vicini l'uno all'altra da provocare un conflitto di memoria.

CASO 3: Per verificare questa ipotesi, ho senza commenti la linea Foo[] arr = array di nuovo, ma questa volta inizializzato il campo array-new Foo[32000] per garantire che la locazione di memoria in fase di scrittura sono sufficientemente distanti tra loro. Quindi, qui stiamo usando l'array assegnato durante la creazione dell'oggetto thread, la differenza con CASE1 è solo che l'array è più grande.

>>> All running times: [2113, 1983, 2430, 2485, 2333, 2359, 2463] 
>>> All running times: [1172, 1106, 1163, 1181, 1142, 1169, 1188] 
>>> All running times: [578, 677, 614, 604, 583, 637, 597] 
>>> All running times: [343, 327, 320, 330, 353, 320, 320] 

Quindi, contesa di memoria sembra essere la causa di questo.

Le informazioni sulla piattaforma:

Ubuntu Server 10.04.3 LTS 
8 core Intel(R) Xeon(R) CPU X5355 @2.66GHz 
~20GB ram 
java version "1.6.0_26" 
Java(TM) SE Runtime Environment (build 1.6.0_26-b03) 
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode) 

Domanda: Questo è ovviamente un problema di memoria-contesa. Ma perché sta succedendo questo?

  1. È in corso l'analisi di evasione? In tal caso, significa che l'intero array viene allocato nello stack quando viene creato nel metodo run in CASE2? Quali sono le condizioni esatte per questa ottimizzazione del runtime? Sicuramente l'array non è allocato nello stack per 1 milione di elementi?

  2. Anche se la matrice viene allocato sullo stack invece di essere allocati in mucchio, due matrice accessi da diversi fili deve essere diviso per almeno 512 * 4bytes = 2kb anche in CASE1, laddove gli array sono ! Questo è decisamente più grande di qualsiasi cache-line L1. Se questi effetti sono dovuti a una condivisione errata, come possono le scritture su più linee di cache totalmente indipendenti influenzare così tanto le prestazioni? (Si suppone qui che ogni array occupi un blocco di memoria contiguo sulla JVM, che viene assegnato quando viene creato l'array. Non sono sicuro che sia valido.Un'altra ipotesi è che le scritture di array non arrivano fino a memoria, ma Cache L1 invece, poiché Intel Xeon ha un'architettura ccNUMA - correggimi se ho torto)

  3. E 'possibile che ogni thread abbia la propria parte di heap locale in cui assegna autonomamente nuovi oggetti, e questo è la causa della contesa inferiore quando l'array è allocato nel thread? In tal caso, in che modo viene raccolta l'area della garbage heap se i riferimenti vengono condivisi?

  4. Perché l'aumento della dimensione della matrice su ~ 32000 elementi ha migliorato la scalabilità (diminuzione della contesa della memoria)? Che cosa esattamente nella gerarchia della memoria è la causa di questo?

Si prega di essere precisi e sostenere le vostre affermazioni con riferimenti.

Grazie!


L'intero programma Java eseguibile:

import java.util.ArrayList; 

class MultiStackJavaExperiment { 

    final class Foo { 
     int x = 0; 
    } 

    final class Worker extends Thread { 
     Foo[] array = new Foo[1024]; 
     int sz; 

     public Worker(int _sz) { 
      sz = _sz; 
     } 

     public void run() { 
      Foo[] arr = new Foo[1024]; 
      //Foo[] arr = array; 
      loop(arr); 
     } 

     public void loop(Foo[] arr) { 
      int i = 0; 
      int pos = 512; 
      Foo v = new Foo(); 
      while (i < sz) { 
       if (i % 2 == 0) { 
        arr[pos] = v; 
        pos += 1; 
       } else { 
        pos -= 1; 
        v = arr[pos]; 
       } 
       i++; 
      } 
     } 
    } 

    public static void main(String[] args) { 
     (new MultiStackJavaExperiment()).mainMethod(args); 
    } 

    int size = Integer.parseInt(System.getProperty("size")); 
    int par = Integer.parseInt(System.getProperty("par")); 

    public void mainMethod(String[] args) { 
     int times = 0; 
     if (args.length == 0) times = 1; 
     else times = Integer.parseInt(args[0]); 
     ArrayList <Long> measurements = new ArrayList <Long>(); 

     for (int i = 0; i < times; i++) { 
      long start = System.currentTimeMillis(); 
      run(); 
      long end = System.currentTimeMillis(); 

      long time = (end - start); 
      System.out.println(i + ") Running time: " + time + " ms"); 
      measurements.add(time); 
     } 

     System.out.println(">>>"); 
     System.out.println(">>> All running times: " + measurements); 
     System.out.println(">>>"); 
    } 

    public void run() { 
     int sz = size/par; 
     ArrayList <Thread> threads = new ArrayList <Thread>(); 

     for (int i = 0; i < par; i++) { 
      threads.add(new Worker(sz)); 
      threads.get(i).start(); 
     } 
     for (int i = 0; i < par; i++) { 
      try { 
       threads.get(i).join(); 
      } catch (Exception e) {} 
     } 
    } 

} 
+0

È facile scherzare con i numeri e ottenere risultati che stai cercando, grazie per dare un'occhiata alla mia risposta. –

+0

Grazie per la risposta, ma perché l'hai cancellata? – axel22

+0

Non ho letto le tue analisi e domande come avrei dovuto e non ho sentito rispondere correttamente alla tua domanda –

risposta

13

Soluzione

Eseguire la JVM con il flag -XX:+UseCondCardMark, disponibile solo in JDK7. Questo risolve il problema.

Spiegazione

In sostanza, la maggior parte degli ambienti gestito heap utilizzano tavoli da gioco per segnare le aree di memoria in cui si sono verificati scrive. Tali aree di memoria sono contrassegnate come sporco nel tavolo da gioco una volta effettuata la scrittura. Questa informazione è necessaria per la garbage collection - i riferimenti delle aree di memoria non sporche non devono essere scansionati. Una scheda è un blocco contiguo di memoria, in genere 512 byte. Normalmente un tavolo da gioco ha 1 byte per ogni carta - se questo byte è impostato, la carta è sporca. Ciò significa che una tabella di carte con 64 byte copre 64 * 512 byte di memoria. E in genere, le dimensioni della linea della cache oggi sono 64 byte.

Ogni volta che si verifica una scrittura in un campo oggetto, il byte della carta corrispondente nel tavolo da gioco deve essere impostato come sporco. Un'ottimizzazione utile nei programmi a thread singolo consiste nel contrassegnare semplicemente il byte pertinente: eseguire una scrittura ogni volta. Un'alternativa di prima verifica se il byte è impostato e una scrittura condizionale richiede una lettura aggiuntiva e un salto condizionato, che è leggermente più lento.

Tuttavia, questa ottimizzazione può essere catastrofica nel caso in cui ci siano più processori che scrivono nella memoria, poiché le carte adiacenti vengono scritte per richiedere una scrittura su byte adiacenti nella tabella delle carte. Quindi l'area di memoria in fase di scrittura (la voce nell'array sopra) non si trova nella stessa cache-line, che è la solita causa di conflitto di memoria. La vera ragione è che i byte sporchi scritti nella stessa cache line.

Ciò che il flag precedente fa è: implementa il byte dirty della tabella della tabella di carte verificando prima se il byte è già impostato e impostandolo solo se non lo è. In questo modo la contesa della memoria si verifica solo durante la prima scrittura su quella scheda, dopodiché si verificano solo le letture su quella riga della cache. Poiché la cache-line viene letta solo, può essere replicata su più processori e non è necessario sincronizzarsi per leggerla.

Ho notato che questo flag aumenta il tempo di esecuzione del 15-20% nel caso 1-thread.

Il flag -XX:+UseCondCardMark è spiegato in questo blog post e questo bug report.

La discussione sulla mailing list relativa alla concorrenza: Array allocation and access on the JVM.

1

credo è necessario ridurre il codice in modo suoi non facendo un sacco di cose accidentali che potrebbero essere le questioni confuse. Dopo aver ridotto il codice, è chiaro che si sta solo accedendo alla stessa posizione dell'array ogni volta. vale a dire la posizione 512.

Se si minimizza il codice, riutilizzare i thread in modo da non fermarli/avviarli si ottengono risultati molto più riproducibili.

import java.util.ArrayList; 
import java.util.Arrays; 
import java.util.List; 
import java.util.concurrent.ExecutionException; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.Future; 

public class MultiStackJavaExperiment { 
    static final int size = Integer.getInteger("size", 500000000); 

    public static void main(String... args) throws ExecutionException, InterruptedException { 
     int par = 8; 
     for (int s = 64; s <= 64 * 1024; s *= 2) { 
      int times = args.length == 0 ? 1 : Integer.parseInt(args[0]); 
      long[] measurements = new long[times]; 

      ExecutorService es = Executors.newFixedThreadPool(par); 
      List<Future<?>> futures = new ArrayList<Future<?>>(times); 
      for (int i = 0; i < times; i++) { 
       long start = System.currentTimeMillis(); 
       final int sz = size/par; 
       futures.clear(); 
       for (int j = 0; j < par; j++) { 
        final Object[] arr = new Object[s]; 
        futures.add(es.submit(new Runnable() { 
         @Override 
         public void run() { 
          final int bits = 7, arraySize = 1 << bits; 
          int i = 0; 
          int pos = 32; 
          Object v = new Object(); 
          while (i < sz) { 
           if (i % 2 == 0) { 
            arr[pos] = v; 
            pos += 1; 
           } else { 
            pos -= 1; 
            v = arr[pos]; 
           } 
           i++; 
          } 
         } 
        })); 
       } 
       for (Future<?> future : futures) 
        future.get(); 

       long time = System.currentTimeMillis() - start; 
//    System.out.println(i + ") Running time: " + time + " ms"); 
       measurements[i] = time; 
      } 
      es.shutdown(); 
      System.out.println("par = " + par + " arr.length= "+ s + " >>> All running times: " + Arrays.toString(measurements)); 
     } 
    } 
} 

Questo mostra la distanza tra i valori di accesso è importante.Allocando un array è ogni filo, si utilizzano diversi TLABs (che distanziare i dati in blocchi)

par = 8 arr.length= 64 >>> All running times: [539, 413, 444, 444, 457, 444, 456] 
par = 8 arr.length= 256 >>> All running times: [398, 527, 514, 529, 445, 441, 445] 
par = 8 arr.length= 1024 >>> All running times: [419, 507, 477, 422, 412, 452, 396] 
par = 8 arr.length= 4096 >>> All running times: [316, 282, 250, 232, 242, 229, 238] 
par = 8 arr.length= 16384 >>> All running times: [316, 207, 209, 212, 208, 208, 208] 
par = 8 arr.length= 65536 >>> All running times: [211, 211, 208, 208, 208, 291, 206] 
par = 8 arr.length= 262144 >>> All running times: [366, 210, 210, 210, 210, 209, 211] 
par = 8 arr.length= 1048576 >>> All running times: [296, 211, 215, 216, 213, 211, 211] 

se si sposta la matrice all'interno del filo si ottiene

par = 8 arr.length= 64 >>> All running times: [225, 151, 151, 150, 152, 153, 152] 
par = 8 arr.length= 256 >>> All running times: [155, 151, 151, 151, 151, 151, 155] 
par = 8 arr.length= 1024 >>> All running times: [153, 152, 151, 151, 151, 155, 152] 
par = 8 arr.length= 4096 >>> All running times: [155, 156, 151, 152, 151, 155, 155] 
par = 8 arr.length= 16384 >>> All running times: [154, 157, 152, 152, 158, 153, 153] 
par = 8 arr.length= 65536 >>> All running times: [155, 157, 152, 184, 181, 154, 153] 
par = 8 arr.length= 262144 >>> All running times: [240, 159, 166, 151, 172, 154, 160] 
par = 8 arr.length= 1048576 >>> All running times: [165, 162, 163, 162, 163, 162, 163] 

Spegnere il TLAB con -XX:-UseTLAB e lo stesso codice danno syou

par = 8 arr.length= 64 >>> All running times: [608, 467, 467, 457, 468, 461, 428] 
par = 8 arr.length= 256 >>> All running times: [437, 437, 522, 512, 522, 369, 535] 
par = 8 arr.length= 1024 >>> All running times: [394, 395, 475, 525, 470, 440, 478] 
par = 8 arr.length= 4096 >>> All running times: [347, 215, 238, 226, 236, 204, 271] 
par = 8 arr.length= 16384 >>> All running times: [291, 157, 178, 151, 150, 151, 152] 
par = 8 arr.length= 65536 >>> All running times: [163, 152, 162, 151, 159, 159, 154] 
par = 8 arr.length= 262144 >>> All running times: [164, 172, 152, 169, 160, 161, 160] 
par = 8 arr.length= 1048576 >>> All running times: [295, 153, 164, 153, 166, 154, 163] 
+0

Grazie per la risposta. 1) Sto accedendo alla stessa posizione, ma in diversi array. 2) Ricevo sempre risultati riproducibili. 3) Gli oggetti thread non vengono raccolti dalla garbage collection: l'ho verificato. Quindi, l'avvio e l'interruzione non dovrebbero influire sulle prestazioni, poiché richiede meno di 120 ms. 4) Prova a spostare questa riga: 'Object [] arr = new Object [1024];' nella classe anonima Runnable che stai creando, in modo che sia un campo della classe. Mi aspetto che non si ridimensiona allora. – axel22

+0

@ axel22 è dentro. Ho aggiunto il risultato per quando è stato spostato all'esterno (vale a dire che l'array è condiviso) Ciò non si adatta bene, come ci si aspetterebbe. Quindi dovresti cambiare il codice per evitare di scrivere da più thread sugli stessi dati. Se questo è impossibile, la soluzione più veloce potrebbe essere quella di utilizzare un thread. –

+0

La matrice può essere allocata: 1) al di fuori del 'Runnable' - quindi è condivisa, 2) all'interno del' Runnable' come campo - quindi è ** non ** condivisa, poiché ogni 'Runnable' ha il proprio array. 3) all'interno del metodo 'run' - quindi è anche ** non ** condiviso. Secondo il tuo codice, è attualmente in fase di allocazione all'interno del metodo 'run'. La mia domanda riguardava la differenza di prestazioni tra '2)' e '3)'. Se ho capito bene, hai confrontato '1)' e '3)'. – axel22

Problemi correlati