newInstance vs nouveau dans jdk-9 / jdk-8 et jmh
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 tant de contrôles de sécurité et utilise la réflexion. Et j'ai décidé de mesurer, d'abord en cours d'exécution 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 font cette différence pas que gros):
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 autour de 2x et bien pire pour le temps de tir unique.
Maintenant, je passe à jdk-9 (build 157 au cas où cela importerait) 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 whooping 50x dans le code chaud. J'utilise la dernière version de jmh (1.19.INSTANTANÉ).
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
Peut quelqu'un a fait la lumière sur pourquoi il y a une si grande différence?
2 réponses
Tout d'Abord, le problème n'a rien à voir avec le système de module (directement).
J'ai remarqué que même avec JDK 9 la première itération d'échauffement 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 a cassé dans la compilation JIT.-XX:+PrintCompilation
confirmé que le benchmark a é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
Puis -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
pointé vers le problème d'inlining:
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 un grand méthode" message signifie que le compilateur n'a pas réussi à intégrer l'appel Class.newInstance
car la taille compilée de l'appelé est supérieure à la valeur InlineSmallCode
(qui est 2000 par défaut).
Quand j'ai relancé le benchmark 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 reviens au GC parallèle, le benchmark sera également rapide même avec le InlineSmallCode
par défaut.
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 un peu 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 par défaut InlineSmallCode
. Une autre raison pour laquelle compiled Class.newInstance
est devenu plus grand est que le code de réflexion a été légèrement réécrit dans JDK 9.
TL; DR JIT n'a pas réussi à intégrer
Class.newInstance
, car la limiteInlineSmallCode
a été dépassée. La version compilée deClass.newInstance
est devenue plus grande en raison des changements dans le code de réflexion dans JDK 9 et parce que le GC par défaut a été changé en G1.
La mise en œuvre de Class.newInstance()
est la plupart du temps identique, sauf 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;
}
}
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;
}
Comme vous pouvez le voir, Java 8 avait un {[3] } 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éfléchissantes avec un type prévisible et Something.class.newInstance()
fait référence à un type parfaitement prévisible. Ce l'optimisation pourrait être devenue moins efficace. Il y a plusieurs raisons possibles:
- les nouvelles règles d'accès au module compliquent le processus
- depuis que
Class.newInstance()
a été déprécié, un certain soutien a été délibérément supprimé (me semble peu probable) - en raison du code d'implémentation modifié montré ci-dessus, HotSpot ne reconnaît pas certains modèles de code qui déclenchent les optimisations