newInstance vs new en jdk-9 / jdk-8 y jmh


He visto muchos hilos aquí que comparan y tratan de responder que es más rápido: newInstance o new operator.

Mirando el código fuente, parecería que newInstance debería ser mucho más lento, quiero decir que hace muchas comprobaciones de seguridad y usa reflexión. Y he decidido medir, primero ejecutando jdk-8. Aquí está el código usando jmh.

@BenchmarkMode(value = { Mode.AverageTime, Mode.SingleShotTime })
@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)   
@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)    
@State(Scope.Benchmark) 
public class TestNewObject {
    public static void main(String[] args) throws RunnerException {

        Options opt = new OptionsBuilder().include(TestNewObject.class.getSimpleName()).build();
        new Runner(opt).run();
    }

    @Fork(1)
    @Benchmark
    public Something newOperator() {
       return new Something();
    }

    @SuppressWarnings("deprecation")
    @Fork(1)
    @Benchmark
    public Something newInstance() throws InstantiationException, IllegalAccessException {
         return Something.class.newInstance();
    }

    static class Something {

    } 
}

No creo que haya grandes sorpresas aquí (JIT hace muchas optimizaciones que hacen esta diferencia no que grande):

Benchmark                  Mode  Cnt      Score      Error  Units
TestNewObject.newInstance  avgt    5      7.762 ±    0.745  ns/op
TestNewObject.newOperator  avgt    5      4.714 ±    1.480  ns/op
TestNewObject.newInstance    ss    5  10666.200 ± 4261.855  ns/op
TestNewObject.newOperator    ss    5   1522.800 ± 2558.524  ns/op

La diferencia para el código caliente sería alrededor de 2x y mucho peor para el tiempo de disparo único.

Ahora cambio a jdk-9 (compilar 157 en caso de que importe) y corro el mismo código. Y los resultados:

 Benchmark                  Mode  Cnt      Score      Error  Units
 TestNewObject.newInstance  avgt    5    314.307 ±   55.054  ns/op
 TestNewObject.newOperator  avgt    5      4.602 ±    1.084  ns/op
 TestNewObject.newInstance    ss    5  10798.400 ± 5090.458  ns/op
 TestNewObject.newOperator    ss    5   3269.800 ± 4545.827  ns/op

Eso es una ferina 50x diferencia en código caliente. Estoy usando la última versión de jmh (1.19.INSTANTÁNEA).

Después de agregar un método más a la prueba:

@Fork(1)
@Benchmark
public Something newInstanceJDK9() throws Exception {
    return Something.class.getDeclaredConstructor().newInstance();
}

Aquí están los resultados generales n jdk-9:

TestNewObject.newInstance      avgt    5    308.342 ±   107.563  ns/op
TestNewObject.newInstanceJDK9  avgt    5     50.659 ±     7.964  ns/op
TestNewObject.newOperator      avgt    5      4.554 ±     0.616  ns/op    

Puede alguien arrojó alguna luz sobre ¿por qué hay una diferencia tan grande?

Author: ZhekaKozlov, 2017-03-14

2 answers

En primer lugar, el problema no tiene nada que ver con el sistema de módulos (directamente).

Noté que incluso con JDK 9 la primera iteración de calentamiento de newInstance fue tan rápida como con JDK 8.

# Fork: 1 of 1
# Warmup Iteration   1: 10,578 ns/op    <-- Fast!
# Warmup Iteration   2: 246,426 ns/op
# Warmup Iteration   3: 242,347 ns/op

Esto significa que algo se ha roto en la compilación JIT.
-XX:+PrintCompilation confirmó que el benchmark fue recompilado después de la primera iteración:

10,762 ns/op
# Warmup Iteration   2:    1541  689   !   3       java.lang.Class::newInstance (160 bytes)   made not entrant
   1548  692 %     4       bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub @ 13 (56 bytes)
   1552  693       4       bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub (56 bytes)
   1555  662       3       bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub (56 bytes)   made not entrant
248,023 ns/op

Luego -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining señaló el problema de inserción:

1577  667 %     4       bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub @ 13 (56 bytes)
                           @ 17   bench.NewInstance::newInstance (6 bytes)   inline (hot)
            !                @ 2   java.lang.Class::newInstance (160 bytes)   already compiled into a big method

"ya compilado en un gran método" mensaje significa que el compilador ha fallado en la llamada inline Class.newInstance porque el tamaño compilado del destinatario es mayor que el valor InlineSmallCode (que es 2000 por defecto).

Cuando volví a analizar el punto de referencia con -XX:InlineSmallCode=2500, se volvió rápido de nuevo.

Benchmark                Mode  Cnt  Score   Error  Units
NewInstance.newInstance  avgt    5  8,847 ± 0,080  ns/op
NewInstance.operatorNew  avgt    5  5,042 ± 0,177  ns/op

Ya sabes, JDK 9 ahora tiene G1 como GC por defecto. Si vuelvo al GC paralelo, el punto de referencia también será rápido incluso con el valor predeterminado InlineSmallCode.

Volver a ejecutar el punto de referencia JDK 9 con -XX:+UseParallelGC:

Benchmark                Mode  Cnt  Score   Error  Units
NewInstance.newInstance  avgt    5  8,728 ± 0,143  ns/op
NewInstance.operatorNew  avgt    5  4,822 ± 0,096  ns/op

G1 requiere poner algunos barreras cada vez que ocurre un almacén de objetos, es por eso que el código compilado se vuelve un poco más grande, de modo que Class.newInstance excede el límite predeterminado InlineSmallCode. Otra razón por la que compiled Class.newInstance se ha vuelto más grande es que el código de reflexión se había reescrito ligeramente en JDK 9.

TL; DR JIT no ha podido insertar Class.newInstance, porque InlineSmallCode se ha excedido el límite. La versión compilada de Class.newInstance se ha hecho más grande debido a los cambios en el código de reflexión en JDK 9 y porque el GC predeterminado ha sido cambiado a G1.

 55
Author: apangin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-15 00:50:48

La implementación de Class.newInstance() es en su mayoría idéntica, excepto la siguiente parte:

Java 8:
Constructor<T> tmpConstructor = cachedConstructor;
// Security check (same as in java.lang.reflect.Constructor)
int modifiers = tmpConstructor.getModifiers();
if (!Reflection.quickCheckMemberAccess(this, modifiers)) {
    Class<?> caller = Reflection.getCallerClass();
    if (newInstanceCallerCache != caller) {
        Reflection.ensureMemberAccess(caller, this, null, modifiers);
        newInstanceCallerCache = caller;
    }
}
Java 9
Constructor<T> tmpConstructor = cachedConstructor;
// Security check (same as in java.lang.reflect.Constructor)
Class<?> caller = Reflection.getCallerClass();
if (newInstanceCallerCache != caller) {
    int modifiers = tmpConstructor.getModifiers();
    Reflection.ensureMemberAccess(caller, this, null, modifiers);
    newInstanceCallerCache = caller;
}

Como puede ver, Java 8 tenía un quickCheckMemberAccess que permitía eludir las operaciones costosas, como Reflection.getCallerClass(). Esta comprobación rápida se ha eliminado, supongo, porque no era compatible con las nuevas reglas de acceso al módulo.

Pero hay más. La JVM podría optimizar instanciaciones reflectantes con un tipo predecible y Something.class.newInstance() se refiere a un tipo perfectamente predecible. Este la optimización podría haberse vuelto menos efectiva. Hay varias razones posibles:

  • las nuevas reglas de acceso al módulo complican el proceso
  • dado que Class.newInstance() ha sido obsoleto, se ha eliminado deliberadamente cierto apoyo (me parece poco probable)
  • debido al código de implementación modificado que se muestra arriba, HotSpot no reconoce ciertos patrones de código que activan las optimizaciones
 4
Author: Holger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-14 16:42:00