39 votes

newInstance vs new dans jdk-9/jdk-8 et jmh

J'ai vu beaucoup de fils de discussion ici qui comparent et essaient de répondre à la question de savoir lequel est le plus rapide : newInstance o 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, d'abord en utilisant 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 (le JIT fait beaucoup d'optimisations qui font que cette différence n'est pas ). si grand ):

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 tir unique.

Maintenant je passe à jdk-9 (build 157 au cas où cela aurait de l'importance) et j'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 un coqueluche 50x différence dans le code chaud. J'utilise la dernière version de jmh (1.19.SNAPSHOT).

Après avoir ajouté une méthode supplémentaire 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 m'éclairer sur pourquoi il y a une si grande différence ?

57voto

apangin Points 4693

Tout d'abord, le problème n'a rien à voir avec le système de modules (directement).

J'ai remarqué que, même avec le JDK 9, la première itération de réchauffement de l'application newInstance était aussi rapide qu'avec le 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 brisé dans la compilation JIT.
-XX:+PrintCompilation a 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 a mis en évidence le problème de l'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 une grande méthode" signifie que le compilateur n'a pas réussi à inline Class.newInstance parce que la taille compilée de l'appelant est plus grande que celle de l'appelant. InlineSmallCode (qui est de 2000 par défaut).

Lorsque j'ai réexécuté 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

Tu sais, JDK 9 a maintenant G1 comme GC par défaut . Si je reviens à la GC parallèle, le benchmark sera également rapide, même avec les paramètres par défaut de la GC parallèle. InlineSmallCode .

Relancer 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 object store se produit, c'est pourquoi le code compilé devient un peu plus gros, de sorte que Class.newInstance dépasse la valeur par défaut InlineSmallCode limite. Une autre raison pour laquelle les compilations Class.newInstance a pris de l'ampleur est que le code de réflexion avait été légèrement réécrit dans le JDK 9.

TL;DR Le JIT n'a pas réussi à mettre en ligne Class.newInstance parce que InlineSmallCode a été dépassée. La version compilée de Class.newInstance est devenu plus grand en raison des changements apportés au code de réflexion dans le JDK 9 et parce que le GC par défaut a été changé en G1.

4voto

Holger Points 13789

La mise en œuvre de Class.newInstance() est pratiquement identique, sauf la partie suivante :

Java 8 :

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 quickCheckMemberAccess ce qui permettait de contourner les opérations coûteuses, telles que Reflection.getCallerClass() . Cette vérification rapide a été supprimée, je suppose, parce qu'elle n'était pas compatible avec les nouvelles règles d'accès aux modules.

Mais il y a plus que ça. La JVM peut optimiser les instanciations réflectives avec un type prévisible et des paramètres d'exécution. Something.class.newInstance() fait référence à un type parfaitement prévisible. Il se peut que cette optimisation soit devenue moins efficace. Il y a plusieurs raisons possibles :

  • les nouvelles règles d'accès aux modules compliquent le processus
  • depuis Class.newInstance() a été déprécié, certains supports ont été délibérément supprimés (ce qui me semble peu probable).
  • en raison de la modification du code d'implémentation indiquée ci-dessus, HotSpot ne parvient pas à reconnaître certains modèles de code qui déclenchent les optimisations

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X