Performances de réflexion Java

Réponses:

169

Oui absolument. Rechercher une classe par réflexion est, par ampleur , plus cher.

Citant la documentation de Java sur la réflexion :

Étant donné que la réflexion implique des types qui sont résolus dynamiquement, certaines optimisations de machine virtuelle Java ne peuvent pas être effectuées. Par conséquent, les opérations réfléchissantes ont des performances plus lentes que leurs homologues non réfléchissantes et doivent être évitées dans les sections de code qui sont fréquemment appelées dans les applications sensibles aux performances.

Voici un test simple que j'ai piraté en 5 minutes sur ma machine, exécutant Sun JRE 6u10:

public class Main {

    public static void main(String[] args) throws Exception
    {
        doRegular();
        doReflection();
    }

    public static void doRegular() throws Exception
    {
        long start = System.currentTimeMillis();
        for (int i=0; i<1000000; i++)
        {
            A a = new A();
            a.doSomeThing();
        }
        System.out.println(System.currentTimeMillis() - start);
    }

    public static void doReflection() throws Exception
    {
        long start = System.currentTimeMillis();
        for (int i=0; i<1000000; i++)
        {
            A a = (A) Class.forName("misc.A").newInstance();
            a.doSomeThing();
        }
        System.out.println(System.currentTimeMillis() - start);
    }
}

Avec ces résultats:

35 // no reflection
465 // using reflection

Gardez à l'esprit que la recherche et l'instanciation sont effectuées ensemble, et dans certains cas, la recherche peut être refactorisée, mais ce n'est qu'un exemple basique.

Même si vous venez d'instancier, vous obtenez toujours un impact sur les performances:

30 // no reflection
47 // reflection using one lookup, only instantiating

Encore une fois, YMMV.

Yuval Adam
la source
5
Sur ma machine, l'appel .newInstance () avec un seul appel Class.forName () marque environ 30. Selon la version de la machine virtuelle, la différence peut être plus proche que vous ne le pensez avec une stratégie de mise en cache appropriée.
Sean Reilly
56
@Peter Lawrey ci-dessous a souligné que ce test était complètement invalide car le compilateur optimisait la solution non réfléchissante (il peut même prouver que rien n'est fait et optimiser la boucle for). Doit être retravaillé et devrait probablement être retiré du SO en tant qu'information mauvaise / trompeuse. Mettez en cache les objets créés dans un tableau dans les deux cas pour empêcher l'optimiseur de l'optimiser. (Il ne peut pas faire cela dans la situation de réflexion car il ne peut pas prouver que le constructeur n'a pas d'effets secondaires)
Bill K
6
@Bill K - ne nous emportons pas. Oui, les chiffres sont décalés en raison des optimisations. Non, le test n'est pas complètement invalide. J'ai ajouté un appel qui supprime toute possibilité de biaiser le résultat, et les chiffres sont toujours empilés contre la réflexion. Dans tous les cas, rappelez-vous qu'il s'agit d'un micro-benchmark très grossier qui montre simplement que la réflexion entraîne toujours un certain surcoût
Yuval Adam
4
C'est probablement une référence inutile. En fonction de ce que fait quelque chose. S'il ne fait rien avec un effet secondaire visible, votre benchmark n'exécute que du code mort.
nes1983
9
Je viens d'assister à l'optimisation de la réflexion de la JVM 35 fois. Exécuter le test à plusieurs reprises dans une boucle permet de tester le code optimisé. Première itération: 3045 ms, deuxième itération: 2941 ms, troisième itération: 90 ms, quatrième itération: 83 ms. Code: c.newInstance (i). c est un constructeur. Code non réfléchissant: nouveau A (i), qui donne 13, 4, 3 .. ms fois. Alors oui, la réflexion dans ce cas était lente, mais pas autant plus lente que ce que les gens concluent, car à chaque test que je vois, ils exécutent simplement le test une fois sans donner à la JVM la possibilité de remplacer les codes octets par la machine code.
Mike
87

Oui, c'est plus lent.

Mais rappelez-vous la putain de règle n ° 1 - L'OPTIMISATION PRÉMATURE EST LA ROOT DE TOUT MAL

(Eh bien, peut être à égalité avec le n ° 1 pour DRY)

Je jure que si quelqu'un s'approchait de moi au travail et me demandait cela, je serais très attentif à leur code pour les prochains mois.

Vous ne devez jamais optimiser jusqu'à ce que vous soyez sûr d'en avoir besoin, jusque-là, écrivez simplement du bon code lisible.

Oh, et je ne veux pas non plus écrire du code stupide. Pensez simplement à la manière la plus propre possible de le faire - pas de copier-coller, etc. (Méfiez-vous toujours de choses comme les boucles internes et utilisez la collection qui correspond le mieux à vos besoins - Ignorer ce n'est pas une programmation «non optimisée» , c'est une "mauvaise" programmation)

Cela me fait peur quand j'entends des questions comme celle-ci, mais j'oublie que tout le monde doit apprendre toutes les règles lui-même avant de vraiment comprendre. Vous l'obtiendrez après avoir passé un mois-homme à déboguer quelque chose de "Optimisé".

ÉDITER:

Une chose intéressante s'est produite dans ce fil. Vérifiez la réponse n ° 1, c'est un exemple de la puissance du compilateur pour optimiser les choses. Le test est complètement invalide car l'instanciation non réfléchissante peut être complètement éliminée.

Leçon? N'optimisez JAMAIS jusqu'à ce que vous ayez écrit une solution propre et soigneusement codée et que vous n'ayez pas prouvé qu'elle était trop lente.

Bill K
la source
28
Je suis tout à fait d'accord avec le sentiment de cette réponse, mais si vous êtes sur le point de vous lancer dans une décision de conception majeure, il est utile d'avoir une idée des performances afin de ne pas vous engager sur une voie totalement impraticable. Peut-être qu'il fait juste une diligence raisonnable?
Système Limbic
26
-1: Éviter de faire les choses de la mauvaise façon n'est pas de l'optimisation, c'est juste faire les choses. L'optimisation fait les choses de manière incorrecte et compliquée en raison de problèmes de performances réels ou imaginaires.
soru
5
@soru est totalement d'accord. Choisir une liste chaînée sur une liste de tableaux pour un tri par insertion est simplement la bonne façon de faire les choses. Mais cette question particulière - il existe de bons cas d'utilisation pour les deux côtés de la question d'origine, donc en choisir un basé sur les performances plutôt que sur la solution la plus utilisable serait faux. Je ne suis pas sûr que nous soyons en désaccord du tout, donc je ne sais pas pourquoi vous avez dit "-1".
Bill K
14
Tout analyste programmeur sensé doit considérer l'efficacité à un stade précoce ou vous pourriez vous retrouver avec un système qui ne peut PAS être optimisé dans un délai efficace et rentable. Non, vous n'optimisez pas chaque cycle d'horloge, mais vous utilisez certainement les meilleures pratiques pour quelque chose d'aussi basique que l'instanciation de classe. Cet exemple est un excellent exemple de POURQUOI vous considérez de telles questions concernant la réflexion. Ce serait un programmeur assez médiocre qui irait de l'avant et utiliserait la réflexion sur un système à un million de lignes pour découvrir plus tard qu'il était trop lent.
RichieHH
2
@Richard Riley En général, l'instanciation de classe est un événement assez rare pour les classes sélectionnées sur lesquelles vous allez réfléchir. Je suppose que vous avez raison cependant - certaines personnes peuvent instancier chaque classe de manière réfléchie, même celles qui sont recréées constamment. J'appellerais cela une programmation plutôt mauvaise (même si même dans ce cas, vous POUVEZ implémenter un cache d'instances de classe à réutiliser après coup et ne pas trop nuire à votre code - alors je suppose que je dirais toujours TOUJOURS concevoir pour la lisibilité, puis profiler et optimiser plus tard)
Bill K
36

Vous pouvez constater que A a = new A () est en cours d'optimisation par la JVM. Si vous placez les objets dans un tableau, ils ne fonctionnent pas très bien. ;) Les impressions suivantes ...

new A(), 141 ns
A.class.newInstance(), 266 ns
new A(), 103 ns
A.class.newInstance(), 261 ns

public class Run {
    private static final int RUNS = 3000000;

    public static class A {
    }

    public static void main(String[] args) throws Exception {
        doRegular();
        doReflection();
        doRegular();
        doReflection();
    }

    public static void doRegular() throws Exception {
        A[] as = new A[RUNS];
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) {
            as[i] = new A();
        }
        System.out.printf("new A(), %,d ns%n", (System.nanoTime() - start)/RUNS);
    }

    public static void doReflection() throws Exception {
        A[] as = new A[RUNS];
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) {
            as[i] = A.class.newInstance();
        }
        System.out.printf("A.class.newInstance(), %,d ns%n", (System.nanoTime() - start)/RUNS);
    }
}

Cela suggère que la différence est d'environ 150 ns sur ma machine.

Peter Lawrey
la source
vous venez de tuer l'optimiseur, donc maintenant les deux versions sont lentes. La réflexion est donc encore sacrément lente.
gbjbaanb
13
@gbjbaanb si l'optimiseur optimisait la création elle-même, ce n'était pas un test valide. Le test de @ Peter est donc valide car il compare en fait les temps de création (L'optimiseur ne pourrait pas fonctionner dans N'IMPORTE QUELLE situation réelle car dans toute situation réelle, vous avez besoin des objets que vous instanciez).
Bill K
10
@ nes1983 Dans ce cas, vous auriez pu profiter de l'occasion pour créer un meilleur benchmark. Peut-être pouvez-vous proposer quelque chose de constructif, comme ce qui devrait être dans le corps de la méthode.
Peter Lawrey
1
sur mon mac, openjdk 7u4, la différence est de 95ns contre 100ns. Au lieu de stocker les A dans le tableau, je stocke les hashCodes. Si vous dites -verbose: class, vous pouvez voir quand hotspot génère du bytecode pour construire A et l'accélération qui l'accompagne.
Ron
@PeterLawrey Si je cherche une fois (un appel à Class.getDeclaredMethod) et que j'appelle ensuite Method.invokeplusieurs fois? Est-ce que j'utilise la réflexion une ou autant de fois que je l'invoque? Question de suivi, que se passe-t-il si au lieu de Methodcela, c'est un Constructoret je le fais Constructor.newInstanceplusieurs fois?
tmj le
28

S'il y a vraiment besoin de quelque chose de plus rapide que la réflexion, et qu'il ne s'agit pas simplement d'une optimisation prématurée, la génération de bytecode avec ASM ou une bibliothèque de niveau supérieur est une option. Générer le bytecode la première fois est plus lent que d'utiliser simplement la réflexion, mais une fois que le bytecode a été généré, il est aussi rapide que le code Java normal et sera optimisé par le compilateur JIT.

Quelques exemples d'applications qui utilisent la génération de code:

  • L'appel de méthodes sur des proxys générés par CGLIB est légèrement plus rapide que les proxys dynamiques de Java , car CGLIB génère du bytecode pour ses proxys, mais les proxys dynamiques utilisent uniquement la réflexion ( j'ai mesuré CGLIB pour être environ 10x plus rapide dans les appels de méthode, mais la création des proxies était plus lente).

  • JSerial génère du bytecode pour lire / écrire les champs des objets sérialisés, au lieu d'utiliser la réflexion. Il y a quelques benchmarks sur le site de JSerial.

  • Je ne suis pas sûr à 100% (et je n'ai pas envie de lire la source maintenant), mais je pense que Guice génère du bytecode pour faire l'injection de dépendances. Corrige moi si je me trompe.

Esko Luontola
la source
27

«Significatif» dépend entièrement du contexte.

Si vous utilisez la réflexion pour créer un objet de gestionnaire unique basé sur un fichier de configuration, puis que vous passez le reste de votre temps à exécuter des requêtes de base de données, cela est insignifiant. Si vous créez un grand nombre d'objets par réflexion dans une boucle serrée, alors oui, c'est important.

En général, la flexibilité de conception (le cas échéant!) Devrait conduire votre utilisation de la réflexion, pas des performances. Cependant, pour déterminer si les performances sont un problème, vous devez établir un profil plutôt que d'obtenir des réponses arbitraires d'un forum de discussion.

kdgregory
la source
24

Il y a une certaine surcharge avec la réflexion, mais c'est beaucoup plus petit sur les machines virtuelles modernes qu'auparavant.

Si vous utilisez la réflexion pour créer chaque objet simple dans votre programme, quelque chose ne va pas. L'utiliser occasionnellement, quand vous avez de bonnes raisons, ne devrait pas du tout être un problème.

Marcus Downing
la source
11

Oui, il y a un impact sur les performances lors de l'utilisation de Reflection, mais une solution de contournement possible pour l'optimisation consiste à mettre la méthode en cache:

  Method md = null;     // Call while looking up the method at each iteration.
      millis = System.currentTimeMillis( );
      for (idx = 0; idx < CALL_AMOUNT; idx++) {
        md = ri.getClass( ).getMethod("getValue", null);
        md.invoke(ri, null);
      }

      System.out.println("Calling method " + CALL_AMOUNT+ " times reflexively with lookup took " + (System.currentTimeMillis( ) - millis) + " millis");



      // Call using a cache of the method.

      md = ri.getClass( ).getMethod("getValue", null);
      millis = System.currentTimeMillis( );
      for (idx = 0; idx < CALL_AMOUNT; idx++) {
        md.invoke(ri, null);
      }
      System.out.println("Calling method " + CALL_AMOUNT + " times reflexively with cache took " + (System.currentTimeMillis( ) - millis) + " millis");

aura pour résultat:

[java] L'appel de la méthode 1000000 fois par réflexe avec recherche a pris 5618 millis

[java] L'appel de la méthode 1000000 fois par réflexe avec cache a pris 270 millis

mel3kings
la source
La réutilisation de méthode / constructeur est en effet utile et aide, mais notez que le test ci-dessus ne donne pas de chiffres significatifs en raison des problèmes habituels de benchmarking (pas de préchauffage, donc la première boucle en particulier mesure principalement le temps de préchauffage JVM / JIT).
StaxMan
7

La réflexion est lente, bien que l'allocation d'objets ne soit pas aussi désespérée que d'autres aspects de la réflexion. Pour obtenir des performances équivalentes avec l'instanciation basée sur la réflexion, vous devez écrire votre code afin que le jit puisse dire quelle classe est instanciée. Si l'identité de la classe ne peut pas être déterminée, le code d'allocation ne peut pas être intégré. Pire encore, l'analyse d'échappement échoue et l'objet ne peut pas être alloué par pile. Si vous avez de la chance, le profilage d'exécution de la JVM peut venir à la rescousse si ce code devient chaud, et peut déterminer dynamiquement quelle classe prédomine et peut optimiser pour celle-là.

Sachez que les microbenchmarks de ce fil sont profondément imparfaits, alors prenez-les avec un grain de sel. Le moins imparfait est de loin celui de Peter Lawrey: il effectue des cycles de préchauffage pour obtenir les méthodes, et il défait (consciemment) l'analyse des évasions pour s'assurer que les allocations se produisent réellement. Même celui-là a ses problèmes, cependant: par exemple, on peut s'attendre à ce que le nombre énorme de magasins de baies vaincre les caches et les tampons de stockage, donc cela finira par être principalement une référence de mémoire si vos allocations sont très rapides. (Félicitations à Peter pour avoir bien conclu: que la différence est "150ns" plutôt que "2,5x". Je soupçonne qu'il fait ce genre de chose pour gagner sa vie.)

Doradus
la source
7

Il est intéressant de noter que le paramètre setAccessible (true), qui ignore les contrôles de sécurité, a une réduction de coût de 20%.

Sans setAccessible (true)

new A(), 70 ns
A.class.newInstance(), 214 ns
new A(), 84 ns
A.class.newInstance(), 229 ns

Avec setAccessible (true)

new A(), 69 ns
A.class.newInstance(), 159 ns
new A(), 85 ns
A.class.newInstance(), 171 ns
Mikhail Kraizman
la source
1
Cela me semble évident en principe. Ces nombres sont-ils mis à l'échelle de manière linéaire lors de l'exécution d' 1000000appels?
Lukas Eder
En fait, cela setAccessible()peut avoir beaucoup plus de différences en général, en particulier pour les méthodes avec plusieurs arguments, il doit donc toujours être appelé.
StaxMan
6

Oui, c'est beaucoup plus lent. Nous exécutions du code qui faisait cela, et bien que je n'ai pas les métriques disponibles pour le moment, le résultat final a été que nous avons dû refactoriser ce code pour ne pas utiliser la réflexion. Si vous savez ce qu'est la classe, appelez simplement le constructeur directement.

Elie
la source
1
+1 J'ai eu une expérience similaire. Il est bon de s'assurer de n'utiliser la réflexion que si c'est absolument nécessaire.
Ryan Thames
Par exemple, les bibliothèques basées sur AOP ont besoin de réflexion.
gaurav
4

Dans le doReflection (), il y a la surcharge due à Class.forName ("misc.A") (qui nécessiterait une recherche de classe, analysant potentiellement le chemin de classe sur le système de fils), plutôt que le newInstance () appelé sur la classe. Je me demande à quoi ressembleraient les statistiques si le Class.forName ("misc.A") n'est fait qu'une seule fois en dehors de la boucle for, il n'est pas vraiment nécessaire de le faire pour chaque invocation de la boucle.

tikoo
la source
1

Oui, il sera toujours plus lent de créer un objet par réflexion car la JVM ne peut pas optimiser le code lors de la compilation. Consultez les didacticiels Sun / Java Reflection pour plus de détails.

Voir ce test simple:

public class TestSpeed {
    public static void main(String[] args) {
        long startTime = System.nanoTime();
        Object instance = new TestSpeed();
        long endTime = System.nanoTime();
        System.out.println(endTime - startTime + "ns");

        startTime = System.nanoTime();
        try {
            Object reflectionInstance = Class.forName("TestSpeed").newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        endTime = System.nanoTime();
        System.out.println(endTime - startTime + "ns");
    }
}
aledbf
la source
3
Notez que vous devez séparer la recherche ( Class.forName()) de l'instanciation (newInstance ()), car leurs caractéristiques de performances varient considérablement et vous pouvez parfois éviter la recherche répétée dans un système bien conçu.
Joachim Sauer le
3
Aussi: vous devez exécuter chaque tâche de nombreuses fois pour obtenir un benchmark utile: tout d'abord, les actions sont trop lentes pour être mesurées de manière fiable et deuxièmement, vous devrez réchauffer la VM HotSpot pour obtenir des chiffres utiles.
Joachim Sauer
1

Souvent, vous pouvez utiliser Apache commons BeanUtils ou PropertyUtils qui introspection (en gros, ils mettent en cache les métadonnées sur les classes afin qu'ils n'aient pas toujours besoin d'utiliser la réflexion).

sproketboy
la source
0

Je pense que cela dépend de la légèreté / lourdeur de la méthode cible. si la méthode cible est très légère (par exemple getter / setter), elle pourrait être 1 ~ 3 fois plus lente. si la méthode cible prend environ 1 milliseconde ou plus, les performances seront très proches. voici le test que j'ai fait avec Java 8 et reflectasm :

public class ReflectionTest extends TestCase {    
    @Test
    public void test_perf() {
        Profiler.run(3, 100000, 3, "m_01 by refelct", () -> Reflection.on(X.class)._new().invoke("m_01")).printResult();    
        Profiler.run(3, 100000, 3, "m_01 direct call", () -> new X().m_01()).printResult();    
        Profiler.run(3, 100000, 3, "m_02 by refelct", () -> Reflection.on(X.class)._new().invoke("m_02")).printResult();    
        Profiler.run(3, 100000, 3, "m_02 direct call", () -> new X().m_02()).printResult();    
        Profiler.run(3, 100000, 3, "m_11 by refelct", () -> Reflection.on(X.class)._new().invoke("m_11")).printResult();    
        Profiler.run(3, 100000, 3, "m_11 direct call", () -> X.m_11()).printResult();    
        Profiler.run(3, 100000, 3, "m_12 by refelct", () -> Reflection.on(X.class)._new().invoke("m_12")).printResult();    
        Profiler.run(3, 100000, 3, "m_12 direct call", () -> X.m_12()).printResult();
    }

    public static class X {
        public long m_01() {
            return m_11();
        }    
        public long m_02() {
            return m_12();
        }    
        public static long m_11() {
            long sum = IntStream.range(0, 10).sum();
            assertEquals(45, sum);
            return sum;
        }    
        public static long m_12() {
            long sum = IntStream.range(0, 10000).sum();
            assertEquals(49995000, sum);
            return sum;
        }
    }
}

Le code de test complet est disponible sur GitHub: ReflectionTest.java

user_3380739
la source