J'ai vu beaucoup de discussions ici qui se comparent et essaient de répondre, ce qui est plus rapide: newInstance
ou new operator
.
En regardant le code source, il semblerait que newInstance
devrait être beaucoup plus lent, je veux dire qu'il fait tellement de contrôles de sécurité et utilise la réflexion. Et j'ai décidé de mesurer, en exécutant d'abord jdk-8. Voici le code utilisant 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 {
}
}
Je ne pense pas qu'il y ait de grandes surprises ici (JIT fait beaucoup d'optimisations qui ne font pas cette différence si 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 différence pour le code chaud serait d'environ 2x et bien pire pour le temps de prise de vue unique.
Maintenant, je passe à jdk-9 (build 157 au cas où cela compte) et exécute le même code. Et les résultats:
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
C'est une différence de coqueluche 50x dans le code chaud. J'utilise la dernière version de jmh (1.19.SNAPSHOT).
Après avoir ajouté une autre méthode au test:
@Fork(1)
@Benchmark
public Something newInstanceJDK9() throws Exception {
return Something.class.getDeclaredConstructor().newInstance();
}
Voici les résultats globaux 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
Quelqu'un peut-il éclairer pourquoi il y a une si grande différence ?
Tout d'abord, le problème n'a rien à voir avec le système de modules (directement).
J'ai remarqué que même avec JDK 9, la première itération de préchauffage de newInstance
était aussi rapide qu'avec 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
Cela signifie que quelque chose s'est cassé dans la compilation JIT.-XX:+PrintCompilation
a confirmé que l'indice de référence avait été recompilé après la première itération:
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
Alors -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
a souligné le problème inline:
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
"déjà compilé dans une grande méthode" message signifie que le compilateur n'a pas réussi à inline Class.newInstance
appel car la taille compilée de l'appelé est supérieure à la valeur InlineSmallCode
(qui est 2000 par défaut).
Quand je relance la référence avec -XX:InlineSmallCode=2500
, il est redevenu rapide.
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
Vous savez, JDK 9 a maintenant G1 comme GC par défaut. Si je retombe sur Parallel GC, le benchmark sera également rapide même avec la valeur par défaut InlineSmallCode
.
Réexécutez le benchmark JDK 9 avec -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 nécessite de mettre des barrières chaque fois qu'un magasin d'objets se produit, c'est pourquoi le code compilé devient un peu plus grand, de sorte que Class.newInstance
dépasse la limite InlineSmallCode
par défaut. Une autre raison pour laquelle compilé Class.newInstance
est devenu plus grand, c'est que le code de réflexion a été légèrement réécrit en JDK 9.
TL; DR JIT n'a pas réussi à intégrer
Class.newInstance
, carInlineSmallCode
limite a été dépassée. La version compilée deClass.newInstance
est devenu plus grand en raison de modifications du code de réflexion dans JDK 9 et parce que le GC par défaut a été changé en G1.
L'implémentation de Class.newInstance()
est essentiellement identique, à l'exception de la partie suivante:
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;
}
}
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;
}
Comme vous pouvez le voir, Java 8 avait un quickCheckMemberAccess
qui permettait de contourner les opérations coûteuses, comme Reflection.getCallerClass()
. Cette vérification rapide a été supprimée, je suppose, car elle n'était pas compatible avec les nouvelles règles d'accès au module.
Mais il y a plus. La JVM peut optimiser les instanciations réflexives avec un type prévisible et Something.class.newInstance()
fait référence à un type parfaitement prévisible. Cette optimisation aurait pu devenir moins efficace. Il y a plusieurs raisons possibles:
Class.newInstance()
est obsolète, une partie du support a été délibérément supprimée (cela me semble peu probable)