2012-01-27 12 views
10

Estoy haciendo algunas pruebas con escape analysis en Java 7 para comprender mejor qué objetos son aptos para apilar la asignación.Elegibilidad para el análisis de escape/asignación de la pila con Java 7

Este es el código que escribí para probar asignación de pila:

import java.util.ArrayList; 
import java.util.Iterator; 


public class EscapeAnalysis { 

    private static final long TIME_TO_TEST = 10L * 1000L; // 10s 

    static class Timestamp { 
     private long millis; 
     public Timestamp(long millis) { 
      this.millis = millis; 
     } 
     public long getTime() { 
      return millis; 
     } 
     public void setTime(long time) { 
      millis = time; 
     } 
    } 

    public static void main(String[] args) { 
     long r = 0; 
     System.out.println("test1"); 
     r += test1(); 
     System.out.println("test2"); 
     r += test2(); 
     System.out.println("test3"); 
     r += test3(); 
     System.out.println("test4"); 
     r += test4(); 
     System.out.println("test5"); 
     r += test5(); 
     System.out.println("test6"); 
     r += test6(); 
     System.out.println(r); 
    } 

    public static long test1() { 
     long r = 0; 
     long start = System.currentTimeMillis(); 
     while (System.currentTimeMillis() - start < TIME_TO_TEST) { 
      r += new Timestamp(System.currentTimeMillis()).getTime(); 
     } 
     return r; 
    } 

    public static long test2() { 
     ArrayList<Integer> l = new ArrayList<Integer>(1000); 
     for (int i = 0; i < 1000; ++i) { 
      l.add(i); 
     } 
     long r = 0; 
     long start = System.currentTimeMillis(); 
     while (System.currentTimeMillis() - start < TIME_TO_TEST) { 
      for (Iterator<Integer> it = l.iterator(); it.hasNext();) { 
       r += it.next().longValue(); 
      } 
     } 
     return r; 
    } 

    public static long test3() { 
     long r = 0; 
     long start = System.currentTimeMillis(); 
     while (System.currentTimeMillis() - start < TIME_TO_TEST) { 
      Timestamp ts = new Timestamp(System.currentTimeMillis()); 
      ts.setTime(42); 
      r += ts.getTime(); 
     } 
     return r; 
    } 

    public static long test4() { 
     ArrayList<Integer> l = new ArrayList<Integer>(1000); 
     for (int i = 0; i < 1000; ++i) { 
      l.add(i); 
     } 
     long r = 0; 
     long start = System.currentTimeMillis(); 
     while (System.currentTimeMillis() - start < TIME_TO_TEST) { 
      Iterator<Integer> it = l.iterator(); 
      r += it.next().longValue(); 
      r += it.next().longValue(); 
      r += it.next().longValue(); 
      r += it.next().longValue(); 
     } 
     return r; 
    } 

    public static long test5() { 
     ArrayList<Integer> l = new ArrayList<Integer>(1000); 
     for (int i = 0; i < 1000; ++i) { 
      l.add(i); 
     } 
     long r = 0; 
     long start = System.currentTimeMillis(); 
     while (System.currentTimeMillis() - start < TIME_TO_TEST) { 
      Iterator<Integer> it = l.iterator(); 
      for (int i = 0; i < l.size(); ++i) { 
       r += it.next().longValue(); 
      } 
     } 
     return r; 
    } 

    public static long test6() { 
     long r = 0; 
     long start = System.currentTimeMillis(); 
     while (System.currentTimeMillis() - start < TIME_TO_TEST) { 
      for (Timestamp ts = new Timestamp(System.currentTimeMillis()); 
        ts.getTime() > 0; 
        ts.setTime(ts.getTime() + System.currentTimeMillis())) { 
       r += ts.getTime(); 
      } 
     } 
     return r; 
    } 

} 

Y aquí es lo que da salida con Java 7 en Linux

java -server -version             
java version "1.7.0_02" 
Java(TM) SE Runtime Environment (build 1.7.0_02-b13) 
Java HotSpot(TM) 64-Bit Server VM (build 22.0-b10, mixed mode) 

java -server -verbose:gc -XX:CompileThreshold=1 -cp bin EscapeAnalysis 
test1 
test2 
[GC 15616K->352K(59776K), 0,0014270 secs] 
[GC 15968K->288K(59776K), 0,0011790 secs] 
[GC 15904K->288K(59776K), 0,0018170 secs] 
[GC 15904K->288K(59776K), 0,0011100 secs] 
[GC 15904K->288K(57152K), 0,0019790 secs] 
[GC 15520K->320K(56896K), 0,0011670 secs] 
[GC 15232K->284K(56256K), 0,0011440 secs] 
test3 
test4 
test5 
[GC 14876K->348K(55936K), 0,0005340 secs] 
[GC 14620K->348K(56000K), 0,0004560 secs] 
[GC 14300K->316K(55296K), 0,0004680 secs] 
[GC 13948K->316K(55488K), 0,0003590 secs] 
[GC 13692K->316K(54784K), 0,0004580 secs] 
[GC 13436K->316K(54976K), 0,0005430 secs] 
[GC 13180K->316K(54272K), 0,0004500 secs] 
[GC 12924K->316K(54464K), 0,0005090 secs] 
[GC 12668K->316K(53760K), 0,0004490 secs] 
[GC 12412K->316K(53888K), 0,0004350 secs] 
[GC 12156K->316K(53312K), 0,0005060 secs] 
test6 
6737499643744733086 

estoy usando registros de GC para conocer si los objetos eran asignado en la pila (idea de Escape analysis in Java) que podría no ser 100% confiable pero parece dar buenos consejos.

Baed en la salida, la asignación de la pila funciona para test1, test3, test4 y test6 y no funciona para test2 y test5. No entiendo por qué esto no funciona con un iterador en el ciclo for aunque funciona

  • con un iterador fuera de un bucle for (ver test4),
  • con otro objeto dentro de un para- loop (ver test6).

He leído el código de la ArrayList iterator y yo no entiendo por qué no sería elegible para asignación de pila en las pruebas 2 y 5 ya que ni escapar el método actual ni el hilo actual.

¿Alguna idea?

+0

podría intentar usar la llamada threadmxbean específica del punto de acceso para obtener los bytes asignados por el hilo después de cada prueba - http://docs.oracle.com/javase/6/docs/jre/api/management/extension/com/ sun/management/ThreadMXBean.html # getThreadAllocatedBytes (long) - como una medida más confiable – Matt

+0

que proporciona el siguiente byte asignado por prueba (test1 - 28344, test2 - 29813408, test3 - 114968, test4 - 411480, test5 - 69673176, test6 - 1368) con EA on y (test1 - 3764906168, test2 - 88636496, test3 - 3867438248, test4 - 6105335224, test5 - 68352808, test6 - 1400) con EA off – Matt

+3

también puede usar una compilación de depuración y '-XX: + UnlockDiagnosticVMOptions - XX: + PrintEscapeAnalysis -XX: + PrintEliminateAllocations' – Matt

Respuesta

8

EA es algo que el compilador C2 analiza en función del IR que genera, por lo tanto, necesita compilar el método antes de disfrutar de los beneficios. Cada prueba se llama una sola vez, por lo que no hay posibilidad de que compile. Los detalles de EA y el IR C2 en la parte interna de hotspot wiki (https://wikis.oracle.com/display/HotSpotInternals/Overview+of+Ideal,+C2 's + alto nivel + + intermedia + representación y https://wikis.oracle.com/display/HotSpotInternals/EscapeAnalysis)

Aquí hay una versión que intenta mostrar el impacto

import com.sun.management.ThreadMXBean; 

import java.lang.management.ManagementFactory; 
import java.util.ArrayList; 
import java.util.Iterator; 


public class EscapeAnalysisTest { 

    private static final long TIME_TO_TEST = 10L * 1000L; // 10s 

    static class Timestamp { 
     private long millis; 

     public Timestamp(long millis) { 
      this.millis = millis; 
     } 

     public long getTime() { 
      return millis; 
     } 

     public void setTime(long time) { 
      millis = time; 
     } 
    } 

    public static void main(String[] args) { 
     System.out.println("****"); 
     doIt(); 
     System.out.println("****"); 
     doIt(); 
     System.out.println("****"); 
     doIt(); 
     System.out.println("****"); 
     doIt(); 
     System.out.println("****"); 
    } 

    private static void doIt() { 
     final ThreadMXBean mxbean = (ThreadMXBean) ManagementFactory.getThreadMXBean(); 
     final long tid = Thread.currentThread().getId(); 
     long r = 0; 
     final long allocPre = mxbean.getThreadAllocatedBytes(tid); 
     r += test1(); 
     long alloc1 = mxbean.getThreadAllocatedBytes(tid); 
     System.out.println("test1 - " + (alloc1 - allocPre)); 
     r += test2(); 
     final long alloc2 = mxbean.getThreadAllocatedBytes(tid); 
     System.out.println("test2 - " + (alloc2 - alloc1)); 
     r += test3(); 
     final long alloc3 = mxbean.getThreadAllocatedBytes(tid); 
     System.out.println("test3 - " + (alloc3 - alloc2)); 
     r += test4(); 
     final long alloc4 = mxbean.getThreadAllocatedBytes(tid); 
     System.out.println("test4 - " + (alloc4 - alloc3)); 
     r += test5(); 
     final long alloc5 = mxbean.getThreadAllocatedBytes(tid); 
     System.out.println("test5 - " + (alloc5 - alloc4)); 
     r += test6(); 
     final long alloc6 = mxbean.getThreadAllocatedBytes(tid); 
     System.out.println("test6 - " + (alloc6 - alloc5)); 
     System.out.println(r); 
    } 

    public static long test1() { 
     long r = 0; 
     long start = System.currentTimeMillis(); 
     while (System.currentTimeMillis() - start < TIME_TO_TEST) { 
      r += new Timestamp(System.currentTimeMillis()).getTime(); 
     } 
     return r; 
    } 

    public static long test2() { 
     ArrayList<Integer> l = new ArrayList<Integer>(1000); 
     for (int i = 0; i < 1000; ++i) { 
      l.add(i); 
     } 
     long r = 0; 
     long start = System.currentTimeMillis(); 
     while (System.currentTimeMillis() - start < TIME_TO_TEST) { 
      for (Iterator<Integer> it = l.iterator(); it.hasNext();) { 
       r += it.next().longValue(); 
      } 
     } 
     return r; 
    } 

    public static long test3() { 
     long r = 0; 
     long start = System.currentTimeMillis(); 
     while (System.currentTimeMillis() - start < TIME_TO_TEST) { 
      Timestamp ts = new Timestamp(System.currentTimeMillis()); 
      ts.setTime(42); 
      r += ts.getTime(); 
     } 
     return r; 
    } 

    public static long test4() { 
     ArrayList<Integer> l = new ArrayList<Integer>(1000); 
     for (int i = 0; i < 1000; ++i) { 
      l.add(i); 
     } 
     long r = 0; 
     long start = System.currentTimeMillis(); 
     while (System.currentTimeMillis() - start < TIME_TO_TEST) { 
      Iterator<Integer> it = l.iterator(); 
      r += it.next().longValue(); 
      r += it.next().longValue(); 
      r += it.next().longValue(); 
      r += it.next().longValue(); 
     } 
     return r; 
    } 

    public static long test5() { 
     ArrayList<Integer> l = new ArrayList<Integer>(1000); 
     for (int i = 0; i < 1000; ++i) { 
      l.add(i); 
     } 
     long r = 0; 
     long start = System.currentTimeMillis(); 
     while (System.currentTimeMillis() - start < TIME_TO_TEST) { 
      Iterator<Integer> it = l.iterator(); 
      for (int i = 0; i < l.size(); ++i) { 
       r += it.next().longValue(); 
      } 
     } 
     return r; 
    } 

    public static long test6() { 
     long r = 0; 
     long start = System.currentTimeMillis(); 
     while (System.currentTimeMillis() - start < TIME_TO_TEST) { 
      for (Timestamp ts = new Timestamp(System.currentTi()); 
       ts.getTime() > 0; 
       ts.setTime(ts.getTime() + System.currentTimeMillis())) { 
       r += ts.getTime(); 
      } 
     } 
     return r; 
    } 

} 

que genera la siguiente resultado cuando se ejecuta con -server -XX:CompileThreshold=1

**** 
test1 - 109048 
test2 - 89243416 
test3 - 16664 
test4 - 42840 
test5 - 71982168 
test6 - 1400 
-5351026995119026839 
**** 
test1 - 16432 
test2 - 85921464 
test3 - 16664 
test4 - 42840 
test5 - 66777600 
test6 - 1368 
7844020592566674506 
**** 
test1 - 48 
test2 - 18256 
test3 - 272 
test4 - 18264 
test5 - 18264 
test6 - 272 
-2137858376905291730 
**** 
test1 - 48 
test2 - 18256 
test3 - 272 
test4 - 18264 
test5 - 18264 
test6 - 272 
3273987624143297143 
**** 

un peligro aquí es que la compilación de este método ha cambiado fundamentalmente, no he tratado de evitar esto por lo que algunos de uso LogCompilation o PrintCompilation puede ser necesario comprobar.

+0

Felicitaciones a @Matt, esto funciona muy bien. En función de su explicación, traté de modificar mi función test2 original para mover la función for-loop a y la asignación de pila funcionó desde la primera ejecución del método. – jpountz

+0

Lo que no entiendo es por qué los recuentos de asignación son mucho más altos con EA apagado incluso antes de la compilación. Se requiere más rascado de la barbilla en ese caso. – Matt

2

El análisis de escape se basa principalmente en la incorporación de llamadas a funciones.

Al igual que con cualquier otra microbenchmark, especialmente en el servidor VM, se requiere calentamiento. Si elimina -XX:CompileThreshold=1 y ejecuta su prueba principal en un bucle, observará que después de 1-2 iteraciones dejará de recoger basura porque el compilador recopiló suficiente información de perfiles para alinear los métodos y luego realizar el análisis de escape.

2

Acabo de investigar lo mismo, pero para Java 8. Puse mi respuesta en una pregunta duplicada ya que no encontré esta a tiempo.

Resumen de the full answer:

En primer lugar, es dependiente de la implementación. Esta respuesta se aplica a OpenJDK 1.8 y probablemente también a Oracle JVM 1.8.

En segundo lugar, como otros han indicado, la asignación de la pila solo ocurre cuando el compilador C2 compila un método, lo que solo ocurre una vez que un método ha sido llamado suficientes veces.

Si es así, los objetos se pueden apilar asignados si se inline

  • todos sus usos
  • nunca se asigna a cualquiera de los campos estáticos o de objetos, sólo para las variables locales
  • en cada punto en el programa, cuyas variables locales contienen referencias al objeto, debe ser determinable en tiempo JIT, y no depender de ningún flujo de control condicional impredecible.
  • Si el objeto es una matriz, su tamaño debe ser constante en tiempo JIT e indexándose debe usar constantes de tiempo JIT.

La alineación especialmente no es predecible si no conoce algunas de las peculiaridades específicas de Hotspot. Vea la respuesta vinculada para algunos detalles.

Cuestiones relacionadas