java - newInstance vs new en jdk-9/jdk-8 y jmh
performance java-8 (2)
He visto muchos hilos aquí que comparan y tratan de responder, lo cual 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 , me refiero a que hace muchas verificaciones de seguridad y usa la 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 que esta diferencia no sea tan 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 aproximadamente 2x y mucho peor para el tiempo de disparo único.
Ahora cambio a jdk-9 (compilación 157 en caso de que importe) y ejecuto 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
Esa es una gran diferencia 50x en código caliente. Estoy usando la última versión de jmh (1.19.SNAPSHOT).
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
¿Alguien puede arrojar algo de luz sobre por qué hay una diferencia tan grande ?
La implementación de Class.newInstance()
es casi idéntica, excepto en 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 que eso. La JVM podría optimizar las instancias de reflexión con un tipo predecible y Something.class.newInstance()
refiere a un tipo perfectamente predecible. Esta optimización podría haberse vuelto menos efectiva. Hay varias razones posibles:
- Las nuevas reglas de acceso al módulo complican el proceso.
- ya que
Class.newInstance()
ha quedado en desuso, se ha eliminado deliberadamente parte del soporte (me parece poco probable) - debido al cambio en el código de implementación que se muestra arriba, HotSpot no reconoce ciertos patrones de código que activan las optimizaciones
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 punto de referencia se volvió a compilar 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
Entonces -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
señaló el problema de la alineació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
el mensaje "ya compilado en un método grande" significa que el compilador no pudo Class.newInstance
llamada Class.newInstance
línea porque el tamaño compilado de la persona llamada es mayor que el valor InlineSmallCode
(que es 2000 por defecto).
Cuando vuelvo a realizar la prueba con -XX:InlineSmallCode=2500
, se volvió rápido nuevamente.
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 el GC predeterminado . Si vuelvo a Parallel GC, el punto de referencia también será rápido incluso con el InlineSmallCode
predeterminado.
Ejecute JDK 9 benchmark 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 algunas barreras cada vez que se produce un almacén de objetos, por eso el código compilado se hace un poco más grande, de modo que Class.newInstance
excede el límite predeterminado de InlineSmallCode
. Otra razón por la que la Class.newInstance
compilada se ha hecho más grande es que el código de reflexión se ha reescrito ligeramente en JDK 9.
TL; DR JIT no ha podido
Class.newInstance
, porque se ha excedido el límite deInlineSmallCode
. La versión compilada deClass.newInstance
ha vuelto más grande debido a los cambios en el código de reflexión en JDK 9 y porque el GC predeterminado se ha cambiado a G1.