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?
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
, porqueInlineSmallCode
se ha excedido el límite. La versión compilada deClass.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.
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:
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
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