2012-01-20 7 views
18

observar la siguiente definición de una subclase de rosca (todo el archivo fuente de Java ejecutable se incluye al final de la pregunta para su conveniencia):asignación de matriz y el acceso a la máquina y la memoria virtual de contención de 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++; 
     } 
    } 
} 

Explicación: el programa comienza -Dpar tales hilos, y establece el sz de cada hilo a -Dsize/-Dpar, donde -Dsize y -Dpar se establecen a través de la línea de comandos cuando se ejecuta el programa. Cada objeto de subproceso tiene un campo array que se inicializa con una nueva matriz de elementos 1024. El razonamiento es que queremos dividir una cantidad igual de trabajo entre un número diferente de subprocesos, esperamos que el programa se escale.

Cada subproceso se inicia y se mide el tiempo necesario para que se completen todos los subprocesos. Hacemos mediciones múltiples para contrarrestar cualquier efecto relacionado con JIT, como se muestra a continuación. Cada hilo hace un bucle. Dentro del ciclo, el hilo lee un elemento en la posición 512 en la matriz en iteraciones pares, y escribe el mismo elemento en 512 en iteraciones impares. Solo las variables locales se modifican de otra manera.

El programa completo está debajo.

Análisis:

probado con -verbose:gc - no hay recolección de basura que se produce durante la ejecución de este programa.

comando Ejecutar:

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

CASO 1: tiempos de funcionamiento de 1,2,4,8 hilos, en ese orden (7 repeticiones):

>>> 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] 

Mi pensamiento era que la escala no lineal se debe a la contención de memoria . Por cierto, las primeras iteraciones realmente funcionan mejor, esto podría deberse al hecho de que en diferentes iteraciones las matrices se asignan en diferentes áreas de memoria.

CASO 2: A continuación, I comentar la línea Foo[] arr = array en el método de la rosca run y asignar una nueva matriz en el método run sí: Foo[] arr = new Foo[1024]. Medidas:

>>> 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] 

Esta vez, todo se escala bastante como se esperaba. No me hubiera imaginado que la ubicación donde se asignó el conjunto juega ningún papel en absoluto, pero obviamente lo hace de alguna manera. Mi idea era que las matrices estaban previamente asignadas tan cerca unas de otras que algo de conflicto de memoria comenzó a suceder.

CASO 3: Para verificar esta hipótesis, no tengo sin comentar la línea Foo[] arr = array de nuevo, pero esta vez inicializado el campo array a new Foo[32000] para asegurar que la ubicación en la memoria están escribiendo son suficientemente lejos unos de otros. Entonces, aquí estamos usando la matriz asignada durante la creación del objeto thread nuevamente, la diferencia con CASE1 es solo que la matriz es más 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] 

Por lo tanto, parece que la causa de esto es la contención de la memoria.

La información de la plataforma:

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) 

Pregunta: Esto es obviamente un problema de memoria en la contención. Pero ¿por qué sucede esto?

  1. ¿Se está realizando el análisis de escape? Si es así, ¿significa que toda la matriz se asigna en la pila cuando se creó en el método run en CASE2? ¿Cuáles son las condiciones exactas para esta optimización de tiempo de ejecución? Sin duda, la matriz no está asignada en la pila para 1 millón de elementos?

  2. Incluso si la matriz se asigna en la pila en lugar de ser asignado en el montón, matriz de dos accesos por diferentes hilos debe dividirse por lo menos 512 * 4bytes = 2 kb incluso en CASE1, donde las matrices son ! Eso es definitivamente más grande que cualquier línea de caché L1. Si estos efectos se deben a la compartición falsa, ¿cómo pueden las escrituras en varias líneas de caché totalmente independientes afectar el rendimiento tanto? (Una suposición aquí es que cada matriz ocupa un bloque contiguo de memoria en la JVM, que se asigna cuando se crea la matriz. No estoy seguro de que esto sea válido. Otra suposición es que las escrituras de la matriz no van hasta el final memoria, pero la caché L1 en cambio, como Intel Xeon tiene una arquitectura ccNUMA - corríjame si estoy equivocado)

  3. Es posible que cada subproceso tenga su propia parte de montón local donde asigna nuevos objetos de forma independiente, y esto ¿Es la causa de menor contención cuando la matriz se asigna en el hilo? De ser así, ¿cómo se recoge esa área de basura acumulada si se comparten las referencias?

  4. ¿Por qué el aumento del tamaño de la matriz a ~ 32000 elementos mejoró la escalabilidad (disminución de la contención de la memoria)? ¿Qué es exactamente en la jerarquía de la memoria la causa de esto?

Por favor, sea preciso y respalde sus reclamos con referencias.

¡Gracias!


Todo el programa Java ejecutable:

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

Es fácil perder el tiempo con los números y obtener los resultados que está buscando, gracias por echar un vistazo a mi respuesta. –

+0

Gracias por la respuesta, pero ¿por qué la eliminaste? – axel22

+0

No he leído su análisis y pregunta tan bien como debería y no creo que haya respondido correctamente a su pregunta –

Respuesta

13

Solución

Ejecute la JVM con el indicador -XX:+UseCondCardMark, disponible solo en JDK7. Esto resuelve el problema.

Explicación

Esencialmente, la mayoría de los entornos montón administrado utilizan mesas de juego para marcar las áreas de memoria en la que se produjeron escribe. Dichas áreas de memoria están marcadas como sucia en la tabla de tarjetas una vez que se produce la escritura. Esta información es necesaria para la recolección de basura: las referencias de las áreas de memoria no sucia no tienen que ser escaneadas. Una tarjeta es un bloque contiguo de memoria, típicamente de 512 bytes. Una tabla de cartas normalmente tiene 1 byte para cada carta; si este byte está configurado, la carta está sucia. Esto significa que una tabla de tarjetas con 64 bytes cubre 64 * 512 bytes de memoria. Y, por lo general, el tamaño de la línea de caché actual es de 64 bytes.

De modo que cada vez que se produce una escritura en un campo de objeto, el byte de la tarjeta correspondiente en la tabla de la tarjeta debe configurarse como sucio. Una optimización útil en programas de un solo hilo es hacer esto simplemente marcando el byte relevante - hacer una escritura cada vez. Una alternativa para verificar primero si el byte está establecido y una escritura condicional requiere una lectura adicional y un salto condicional, que es un poco más lento.

Sin embargo, esta optimización puede ser catastrófica en el caso de que haya múltiples procesadores escribiendo en la memoria, ya que las tarjetas vecinas se escriben para requerir una escritura en los bytes contiguos en la tabla de cartas. Por lo tanto, el área de memoria en la que se está escribiendo (la entrada en la matriz de arriba) no está en la misma línea de caché, que es la causa habitual de contención de memoria. La verdadera razón es que los bytes sucios que se escriben están en la misma línea de caché.

Lo que hace la bandera de arriba es - implementa la escritura de byte sucio de la tabla de la tarjeta comprobando primero si el byte ya está configurado, y configurándolo solo si no lo está. De esta forma, la contención de la memoria ocurre solo durante la primera escritura en esa tarjeta; después de eso, solo se producen lecturas en esa línea de caché. Como la línea de caché solo se lee, puede replicarse en varios procesadores y no es necesario sincronizarla para leerla.

He observado que este indicador aumenta el tiempo de ejecución en un 15-20% en el caso de 1 hilo.

La bandera -XX:+UseCondCardMark se explica en este blog post y este bug report.

La discusión de la lista de correo de concurrencia relevante: Array allocation and access on the JVM.

1

creo que necesita para reducir su código para sus lotes no haciendo cosas accidentales que podrían ser asuntos confusos. Después de reducir el código, me queda claro que solo está accediendo a la misma ubicación de matriz cada vez. es decir, posición 512.

Si minimiza su código, reutilice sus hilos para que no los detenga/inicie, obtendrá resultados mucho más reproducibles.

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

esto muestra la distancia entre los valores de acceso.Mediante la asignación de un array es cada hilo, se utilizan diferentes TLABs (que espaciar los datos en bloques)

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] 

Si mueve la matriz dentro de la rosca se obtiene

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] 

Apague el TLAB con -XX:-UseTLAB y el mismo código syou dan

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

Gracias por su respuesta. 1) Estoy accediendo a la misma posición, pero en diferentes arreglos. 2) Estoy obteniendo resultados reproducibles cada vez. 3) Los objetos de subprocesos no se están recogiendo basura. Lo he comprobado. Por lo tanto, iniciarlos y detenerlos no debería afectar el rendimiento, ya que eso lleva mucho menos de 120 ms. 4) Intente mover esta línea: 'Object [] arr = new Object [1024];' dentro de la clase anónima 'Runnable' que está creando, de modo que sea un campo de la clase. Supongo que no se escalará entonces. – axel22

+0

@ axel22 está adentro. He agregado el resultado para cuando se movió fuera (es decir, la matriz se comparte) Esto no se escala bien, como era de esperar. Por lo tanto, podría cambiar el código para evitar tener escrituras de múltiples hilos en los mismos datos. Si eso es imposible, la solución más rápida puede ser usar un hilo. –

+0

La matriz se puede asignar: 1) fuera del 'Runnable' - luego se comparte, 2) dentro del' Runnable' como un campo - luego es ** no ** compartido, ya que cada 'Runnable' tiene su propia matriz. 3) dentro del método 'ejecutar' - luego también ** ** compartido. De acuerdo con su código, actualmente se está asignando dentro del método 'run'. Mi pregunta era sobre la diferencia de rendimiento entre '2)' y '3)'. Si entendí correctamente, has comparado '1)' y '3)'. – axel22

Cuestiones relacionadas