50 votes

Java Reflection : Pourquoi est-ce si lent ?

J'ai toujours évité les réflexions en Java, uniquement en raison de sa réputation de lenteur. J'ai atteint un point dans la conception de mon projet actuel où être capable de l'utiliser rendrait mon code beaucoup plus lisible et élégant, alors j'ai décidé de l'essayer.

J'ai été tout simplement étonné par la différence, j'ai remarqué des temps d'exécution parfois presque 100x plus longs. Même dans cet exemple simple où il suffit d'instancier une classe vide, c'est incroyable.

class B {

}

public class Test {

    public static long timeDiff(long old) {
        return System.currentTimeMillis() - old;
    }

    public static void main(String args[]) throws Exception {

        long numTrials = (long) Math.pow(10, 7);

        long millis;

        millis = System.currentTimeMillis();

        for (int i=0; i<numTrials; i++) {
            new B();
        }
        System.out.println("Normal instaniation took: "
                 + timeDiff(millis) + "ms");

        millis = System.currentTimeMillis();

        Class<B> c = B.class;

        for (int i=0; i<numTrials; i++) {
            c.newInstance();
        }

        System.out.println("Reflecting instantiation took:" 
              + timeDiff(millis) + "ms");

    }
}

Donc en fait, mes questions sont

  • Pourquoi est-ce si lent ? Y a-t-il quelque chose que je fais mal ? (même l'exemple ci-dessus montre la différence). J'ai du mal à croire que cela puisse vraiment être 100x plus lent qu'une instanciation normale.

  • Y a-t-il quelque chose d'autre qui puisse être mieux utilisé pour traiter le code comme des données (n'oubliez pas que je suis coincé avec Java pour le moment) ?

1 votes

Il y a également un fil de discussion à stackoverflow.com/questions/435553/java-reflection-performance qui vous incite à lire la documentation de l'API de réflexion

2 votes

L'évaluation comparative de la JVM est difficile. La JVM de Hotspot ne se laissera pas berner par un test naïf comme celui-ci qui ne fait rien.

0 votes

Il faut juste noter que c'est une très mauvaise façon de faire des benchmarks, même pour un microbenchmark.

47voto

oxbow_lakes Points 70013

La réflexion est lente pour quelques raisons évidentes :

  1. Le compilateur ne peut effectuer aucune optimisation car il n'a aucune idée réelle de ce que vous faites. Cela vaut probablement pour le JIT également
  2. Tout ce qui est invoqué/créé doit être a découvert (par exemple, recherche de classes par leur nom, recherche de méthodes correspondantes, etc.)
  3. Les arguments doivent être mis en forme par le biais de la mise en boîte et de la mise hors boîte, de l'emballage dans des tableaux, Exceptions enveloppé dans InvocationTargetException et jetés à nouveau, etc.
  4. Tout le traitement que Jon Skeet mentions ici .

Juste parce que quelque chose est 100x plus lent ne signifie pas qu'il est trop lent pour vous en supposant que la réflexion est la "bonne façon" pour vous de concevoir votre programme. Par exemple, j'imagine que les IDE font un usage intensif de la réflexion et que mon IDE est plutôt correct du point de vue des performances.

Après tout, le tête de réflexion est susceptible de être insignifiant quand par rapport à par exemple, l'analyse syntaxique de XML ou accès à une base de données !

Un autre point à retenir est que Les micro-benchmarks sont un mécanisme notoirement imparfait pour déterminer la rapidité d'un produit dans la pratique. . Ainsi que Tim Bender remarques la JVM prend du temps pour "chauffer", le JIT peut réoptimiser les points chauds du code à la volée, etc.

11 votes

On ne saurait trop insister sur le fait que l'utilisation de la réflexion n'est que lente par rapport à l'utilisation du code équivalent sans réflexion. La lecture ou l'écriture sur le disque dur, l'analyse du xml, l'utilisation du réseau ou l'accès à une base de données (qui accède au disque dur et au réseau) sont tous plus lents que la réflexion.

0 votes

Reflection peut également accéder au disque dur.

3 votes

Je viens de voir la JVM optimiser la réflexion 35 fois. Exécuter le test de façon répétée dans une boucle est la façon de tester un code optimisé. Première itération : 3045ms, deuxième itération : 2941ms, troisième itération : 90ms, quatrième itération : 83ms. Code : c.newInstance(i). c est un Constructeur. Code non réfléchi : new A(i), qui donne des temps de 13, 4, 3 ms. Donc oui, la réflexion dans ce cas était lente, mais pas autant que ce que les gens concluent, parce que tous les tests que je vois, ils exécutent simplement le test une fois sans donner à la JVM l'occasion de remplacer les codes d'octets par du code machine.

41voto

Tim Bender Points 11611

Votre test est peut-être défectueux. En général, la JVM peut optimiser l'instanciation normale mais ne peut pas faire d'optimisations pour le cas d'utilisation réfléchi. .

Pour ceux qui se demandent quels étaient les temps, j'ai ajouté une phase d'échauffement et utilisé un tableau pour maintenir les objets créés (plus similaire à ce qu'un vrai programme pourrait faire). J'ai exécuté le code de test sur mon système OSX, jdk7 et j'ai obtenu ceci :

L'instanciation de réflexion a pris:5180ms
L'installation normale a été faite : 2001ms

Test modifié :

public class Test {

    static class B {

    }

    public static long timeDiff(long old) {
        return System.nanoTime() - old;
    }

    public static void main(String args[]) throws Exception {

        int numTrials = 10000000;
        B[] bees = new B[numTrials];
        Class<B> c = B.class;
        for (int i = 0; i < numTrials; i++) {
            bees[i] = c.newInstance();
        }
        for (int i = 0; i < numTrials; i++) {
            bees[i] = new B();
        }

        long nanos;

        nanos = System.nanoTime();
        for (int i = 0; i < numTrials; i++) {
            bees[i] = c.newInstance();
        }
        System.out.println("Reflecting instantiation took:" + TimeUnit.NANOSECONDS.toMillis(timeDiff(nanos)) + "ms");

        nanos = System.nanoTime();
        for (int i = 0; i < numTrials; i++) {
            bees[i] = new B();
        }
        System.out.println("Normal instaniation took: " + TimeUnit.NANOSECONDS.toMillis(timeDiff(nanos)) + "ms");
    }

}

1 votes

Avez-vous une idée de la raison pour laquelle le temps d'instanciation normal varie si fortement ?

3 votes

Non. J'ai remarqué la différence et me suis demandé la même chose, mais j'ai choisi de ne pas enquêter. BTW, j'ai découvert que c'est une répétition de : stackoverflow.com/questions/435553/java-reflection-performance ...et je soutiens à la fois la réponse principale et la contribution de Marcus Downing comme deuxième réponse.

0 votes

J'espère vraiment que la JVM ne fait pas cette optimisation si le constructeur de B a un effet secondaire ?

29voto

Jon Skeet Points 692016

Le code JIT pour l'instanciation de B est le suivant incroyablement léger. En gros, il doit allouer suffisamment de mémoire (ce qui revient à incrémenter un pointeur à moins qu'un GC ne soit nécessaire) et c'est à peu près tout - il n'y a pas vraiment de code constructeur à appeler ; je ne sais pas si le JIT le saute ou non, mais dans tous les cas, il n'y a pas grand chose à faire.

Comparez cela avec tout ce que la réflexion doit faire :

  • Vérifier qu'il y a un constructeur sans paramètre
  • Vérifier l'accessibilité du constructeur sans paramètre
  • Vérifier que l'appelant a le droit d'utiliser la réflexion.
  • Déterminer (au moment de l'exécution) l'espace à allouer.
  • Appel dans le code du constructeur (car il ne saura pas à l'avance que le constructeur est vide).

... et probablement d'autres choses auxquelles je n'ai pas encore pensé.

En général, la réflexion n'est pas utilisée dans un contexte de performances critiques ; si vous avez besoin d'un comportement dynamique de ce type, vous pouvez utiliser quelque chose comme BCEL à la place.

11 votes

Si l'on construit B ne fait rien, on peut l'enlever complètement. zOMG, la réflexion est infiniment plus lente !

10voto

Esko Luontola Points 53877

Il semble que si vous rendez le constructeur accessible, il s'exécutera beaucoup plus rapidement. Maintenant, il est seulement 10 à 20 fois plus lent que l'autre version.

    Constructor<B> c = B.class.getDeclaredConstructor();
    c.setAccessible(true);
    for (int i = 0; i < numTrials; i++) {
        c.newInstance();
    }

Normal instaniation took: 47ms
Reflecting instantiation took:718ms

Et si vous utilisez le Server VM, il est capable de l'optimiser davantage, de sorte qu'il n'est que 3 à 4 fois plus lent. C'est une performance tout à fait typique. L'article que Geo a lié est une bonne lecture.

Normal instaniation took: 47ms
Reflecting instantiation took:140ms

Mais si vous activez le remplacement scalaire avec -XX:+DoEscapeAnalysis, la JVM est capable d'optimiser l'instanciation régulière (0-15ms) mais l'instanciation réfléchie reste la même.

0 votes

Maintenant, je suis curieux, juste parce que le blog est actuellement inaccessible aux lecteurs non-invités (oui, WTF ). Je ne code même pas en Java, mais je suis devenu curieux simplement pour la différence de performance.

2voto

Tempus Points 22972

Peut-être que vous trouverez cet article intéressant.

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