web-dev-qa-db-fra.com

newInstance vs new en 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 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 ?

39
Eugene

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, car InlineSmallCode limite a été dépassée. La version compilée de Class.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.

57
apangin

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:

  • les nouvelles règles d'accès au module compliquent le processus
  • depuis que Class.newInstance() est obsolète, une partie du support a été délibérément supprimée (cela me semble peu probable)
  • en raison du code d'implémentation modifié illustré ci-dessus, HotSpot ne reconnaît pas certains modèles de code qui déclenchent les optimisations
4
Holger