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?

38
demandé sur ZhekaKozlov 2017-03-14 15:55:37

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

55
répondu apangin 2017-03-15 00:50:48

La mise en œuvre de Class.newInstance() est la plupart du temps 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 {[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
4
répondu Holger 2017-03-14 16:42:00