Java Reflection-Leistung

Antworten:

169

Ja absolut. Das Nachschlagen einer Klasse durch Reflexion erfolgt nach Größenordnung teurer.

Zitieren von Javas Dokumentation zur Reflexion :

Da es sich bei der Reflektion um Typen handelt, die dynamisch aufgelöst werden, können bestimmte Optimierungen der virtuellen Java-Maschine nicht durchgeführt werden. Folglich haben reflektierende Operationen eine langsamere Leistung als ihre nicht reflektierenden Gegenstücke und sollten in Codeabschnitten vermieden werden, die in leistungsempfindlichen Anwendungen häufig aufgerufen werden.

Hier ist ein einfacher Test, den ich in 5 Minuten auf meinem Computer mit Sun JRE 6u10 gehackt habe:

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);
    }
}

Mit diesen Ergebnissen:

35 // no reflection
465 // using reflection

Denken Sie daran, dass die Suche und die Instanziierung zusammen durchgeführt werden. In einigen Fällen kann die Suche auch überarbeitet werden. Dies ist jedoch nur ein einfaches Beispiel.

Selbst wenn Sie nur instanziieren, erhalten Sie dennoch einen Leistungseinbruch:

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

Wieder YMMV.

Yuval Adam
quelle
5
Auf meinem Computer erzielt der Aufruf von .newInstance () mit nur einem Aufruf von Class.forName () ungefähr 30 Punkte. Abhängig von der VM-Version kann der Unterschied bei einer geeigneten Caching-Strategie größer sein als Sie denken.
Sean Reilly
56
@ Peter Lawrey unten wies darauf hin, dass dieser Test vollständig ungültig war, weil der Compiler die nicht reflektierende Lösung optimierte (es kann sogar beweisen, dass nichts getan wird, und die for-Schleife optimieren). Muss überarbeitet werden und sollte wahrscheinlich als schlechte / irreführende Information aus SO entfernt werden. Zwischenspeichern Sie die erstellten Objekte in beiden Fällen in einem Array, um zu verhindern, dass der Optimierer sie optimiert. (Es kann dies nicht in der reflektierenden Situation tun, weil es nicht beweisen kann, dass der Konstruktor keine Nebenwirkungen hat)
Bill K
6
@ Bill K - lassen wir uns nicht mitreißen. Ja, die Zahlen sind aufgrund von Optimierungen deaktiviert. Nein, der Test ist nicht vollständig ungültig. Ich habe einen Aufruf hinzugefügt, der jede Möglichkeit beseitigt, das Ergebnis zu verzerren, und die Zahlen sind immer noch gegen Reflexion gestapelt. Denken Sie auf jeden Fall daran, dass dies ein sehr grober Mikro-Benchmark ist, der nur zeigt, dass die Reflexion immer einen gewissen Aufwand verursacht
Yuval Adam,
4
Dies ist wahrscheinlich ein nutzloser Maßstab. Je nachdem, was doSomething macht. Wenn es nichts mit sichtbaren Nebenwirkungen tut, führt Ihr Benchmark nur toten Code aus.
nes1983
9
Ich habe gerade gesehen, wie die JVM die Reflexion 35-fach optimiert. Wenn Sie den Test wiederholt in einer Schleife ausführen, testen Sie optimierten Code. Erste Iteration: 3045 ms, zweite Iteration: 2941 ms, dritte Iteration: 90 ms, vierte Iteration: 83 ms. Code: c.newInstance (i). c ist ein Konstruktor. Nicht reflektierender Code: neues A (i), das 13, 4, 3 .. ms mal ergibt. Ja, die Reflexion war in diesem Fall langsam, aber nicht annähernd so langsam wie das, was die Leute zu dem Schluss kommen, denn bei jedem Test, den ich sehe, führen sie den Test einfach einmal aus, ohne der JVM die Möglichkeit zu geben, Bytecodes durch Maschine zu ersetzen Code.
Mike
87

Ja, es ist langsamer.

Aber denken Sie an die verdammte Regel Nr. 1: OPEMATUROPTIMIERUNG IST DIE WURZEL ALLEN BÖSEN

(Nun, kann mit # 1 für DRY verbunden sein)

Ich schwöre, wenn jemand bei der Arbeit auf mich zukam und mich danach fragte, würde ich in den nächsten Monaten sehr wachsam über ihren Code sein.

Sie dürfen niemals optimieren, bis Sie sicher sind, dass Sie es brauchen. Bis dahin schreiben Sie einfach guten, lesbaren Code.

Oh, und ich meine auch nicht, dummen Code zu schreiben. Denken Sie nur an die sauberste Art und Weise, wie Sie dies tun können - kein Kopieren und Einfügen usw. (Seien Sie immer noch vorsichtig bei Dingen wie inneren Schleifen und verwenden Sie die Sammlung, die Ihren Anforderungen am besten entspricht. Das Ignorieren dieser Programme ist keine "nicht optimierte" Programmierung , es ist "schlechte" Programmierung)

Es macht mich verrückt, wenn ich Fragen wie diese höre, aber dann vergesse ich, dass jeder alle Regeln selbst lernen muss, bevor er sie wirklich versteht. Sie erhalten es, nachdem Sie einen Monat lang mit dem Debuggen von etwas "Optimiertem" verbracht haben.

BEARBEITEN:

In diesem Thread ist etwas Interessantes passiert. Überprüfen Sie die Antwort Nr. 1. Dies ist ein Beispiel dafür, wie leistungsfähig der Compiler bei der Optimierung von Dingen ist. Der Test ist vollständig ungültig, da die nicht reflektierende Instanziierung vollständig herausgerechnet werden kann.

Lektion? Optimieren Sie NIEMALS, bis Sie eine saubere, sauber codierte Lösung geschrieben und bewiesen haben, dass sie zu langsam ist.

Bill K.
quelle
28
Ich stimme dem Gefühl dieser Antwort voll und ganz zu. Wenn Sie jedoch eine wichtige Designentscheidung treffen möchten, ist es hilfreich, eine Vorstellung von der Leistung zu haben, damit Sie nicht auf einen völlig unbrauchbaren Weg gehen. Vielleicht macht er nur Due Diligence?
Limbisches System
26
-1: Vermeiden, Dinge falsch zu machen, ist keine Optimierung, sondern nur Dinge zu tun. Bei der Optimierung werden die Dinge aufgrund realer oder imaginärer Leistungsprobleme falsch und kompliziert ausgeführt.
Soru
5
@ Soru stimme voll und ganz zu. Das Auswählen einer verknüpften Liste gegenüber einer Array-Liste für eine Einfügesortierung ist einfach der richtige Weg, um Dinge zu tun. Aber diese spezielle Frage - es gibt gute Anwendungsfälle für beide Seiten der ursprünglichen Frage, daher wäre es falsch, einen zu wählen, der eher auf der Leistung als auf der am besten verwendbaren Lösung basiert. Ich bin mir nicht sicher, ob wir überhaupt nicht einverstanden sind, deshalb bin ich mir nicht sicher, warum Sie "-1" gesagt haben.
Bill K
14
Jeder vernünftige Analystenprogrammierer muss die Effizienz frühzeitig in Betracht ziehen, da Sie sonst möglicherweise ein System erhalten, das NICHT in einem effizienten und kostspieligen Zeitraum optimiert werden kann. Nein, Sie optimieren nicht jeden Taktzyklus, aber Sie wenden mit Sicherheit Best Practices für etwas so Grundlegendes wie die Klasseninstanziierung an. Dieses Beispiel ist ein gutes Beispiel dafür, warum Sie solche Fragen in Bezug auf Reflexion berücksichtigen. Es wäre ein ziemlich armer Programmierer, der die Reflexion in einem Millionen-Linien-System nutzte, um später festzustellen, dass es um Größenordnungen zu langsam war.
RichieHH
2
@Richard Riley Im Allgemeinen ist die Instanziierung von Klassen ein ziemlich seltenes Ereignis für die ausgewählten Klassen, für die Sie Reflexion verwenden. Ich nehme an, Sie haben Recht - einige Leute instanziieren möglicherweise jede Klasse nachdenklich, selbst solche, die ständig neu erstellt werden. Ich würde das als ziemlich schlechte Programmierung bezeichnen (obwohl Sie selbst dann einen Cache mit Klasseninstanzen implementieren könnten, um sie nachträglich wiederzuverwenden und Ihren Code nicht zu sehr zu beschädigen - also würde ich immer noch IMMER für Lesbarkeit entwerfen, dann profilieren und optimieren später)
Bill K
36

Möglicherweise stellen Sie fest, dass A a = new A () von der JVM optimiert wird. Wenn Sie die Objekte in ein Array einfügen, sind sie nicht so leistungsfähig. ;) Die folgenden Drucke ...

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);
    }
}

Dies deutet darauf hin, dass der Unterschied auf meinem Computer etwa 150 ns beträgt.

Peter Lawrey
quelle
Sie haben gerade den Optimierer beendet, und jetzt sind beide Versionen langsam. Die Reflexion ist daher immer noch verdammt langsam.
Gbjbaanb
13
@gbjbaanb Wenn der Optimierer die Erstellung selbst optimiert hat, war dies kein gültiger Test. Der Test von @ Peter ist daher gültig, da er tatsächlich die Erstellungszeiten vergleicht (Der Optimierer kann in keiner realen Situation arbeiten, da Sie in jeder realen Situation die Objekte benötigen, die Sie instanziieren).
Bill K
10
@ nes1983 In diesem Fall hätten Sie die Gelegenheit nutzen können, um einen besseren Benchmark zu erstellen. Vielleicht können Sie etwas Konstruktives anbieten, wie das, was im Hauptteil der Methode enthalten sein sollte.
Peter Lawrey
1
Auf meinem Mac, openjdk 7u4, beträgt der Unterschied 95 ns gegenüber 100 ns. Anstatt A's im Array zu speichern, speichere ich HashCodes. Wenn Sie -verbose: class sagen, können Sie sehen, wann der Hotspot Bytecode für die Erstellung von A und die damit verbundene Beschleunigung generiert.
Ron
@PeterLawrey Wenn ich einmal nachschaue (ein Anruf bei Class.getDeclaredMethod) und dann Method.invokemehrmals anrufe ? Benutze ich die Reflexion einmal oder so oft, wie ich sie aufrufe? Folgefrage, was ist, wenn es stattdessen Methodeine ist Constructorund ich es Constructor.newInstancemehrmals mache ?
tmj
28

Wenn wirklich etwas schneller als Reflexion benötigt wird und es nicht nur eine vorzeitige Optimierung ist, dann die Bytecode-Generierung mit ASM oder einer höheren Bibliothek eine Option. Das erstmalige Generieren des Bytecodes ist langsamer als nur das Reflektieren. Sobald der Bytecode generiert wurde, ist er genauso schnell wie normaler Java-Code und wird vom JIT-Compiler optimiert.

Einige Beispiele für Anwendungen, die die Codegenerierung verwenden:

  • Das Aufrufen von Methoden für von CGLIB generierte Proxys ist etwas schneller als die dynamischen Proxys von Java , da CGLIB Bytecode für seine Proxys generiert, dynamische Proxys jedoch nur Reflexion verwenden ( ich habe gemessen, dass CGLIB bei Methodenaufrufen etwa 10-mal schneller ist, aber das Erstellen der Proxys war langsamer).

  • JSerial generiert einen Bytecode zum Lesen / Schreiben der Felder serialisierter Objekte, anstatt Reflektion zu verwenden. Es gibt einige Benchmarks auf der Website von JSerial.

  • Ich bin nicht 100% sicher (und ich habe jetzt keine Lust, die Quelle zu lesen), aber ich denke, Guice generiert Bytecode, um die Abhängigkeitsinjektion durchzuführen. Korrigiere mich, wenn ich falsch liege.

Esko Luontola
quelle
27

"Signifikant" ist völlig kontextabhängig.

Wenn Sie Reflection verwenden, um ein einzelnes Handlerobjekt basierend auf einer Konfigurationsdatei zu erstellen, und dann den Rest Ihrer Zeit damit verbringen, Datenbankabfragen auszuführen, ist dies unbedeutend. Wenn Sie eine große Anzahl von Objekten durch Reflexion in einer engen Schleife erstellen, ist dies von Bedeutung.

Im Allgemeinen sollte die Designflexibilität (wo erforderlich!) Die Verwendung von Reflexion und nicht die Leistung fördern. Um jedoch festzustellen, ob die Leistung ein Problem darstellt, müssen Sie ein Profil erstellen, anstatt willkürliche Antworten von einem Diskussionsforum zu erhalten.

kdgregory
quelle
24

Es gibt einen gewissen Overhead bei der Reflexion, aber auf modernen VMs ist er viel kleiner als früher.

Wenn Sie Reflektion verwenden, um jedes einfache Objekt in Ihrem Programm zu erstellen, stimmt etwas nicht. Es gelegentlich zu verwenden, wenn Sie einen guten Grund haben, sollte überhaupt kein Problem sein.

Marcus Downing
quelle
11

Ja, es gibt einen Leistungseinbruch bei der Verwendung von Reflection, aber eine mögliche Problemumgehung für die Optimierung ist das Zwischenspeichern der Methode:

  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");

wird darin enden, dass:

[Java] Das 1000000-malige reflexive Aufrufen der Methode mit Lookup dauerte 5618 Millis

[Java] Das 1000000-malige reflexive Aufrufen der Methode mit dem Cache dauerte 270 Millis

mel3kings
quelle
Die Wiederverwendung von Methode / Konstruktor ist in der Tat nützlich und hilfreich. Beachten Sie jedoch, dass der obige Test aufgrund der üblichen Benchmarking-Probleme keine aussagekräftigen Zahlen liefert (kein Aufwärmen, daher misst insbesondere die erste Schleife hauptsächlich die JVM / JIT-Aufwärmzeit).
StaxMan
7

Die Reflexion ist langsam, obwohl die Objektzuweisung nicht so hoffnungslos ist wie andere Aspekte der Reflexion. Um eine gleichwertige Leistung mit reflexionsbasierter Instanziierung zu erzielen, müssen Sie Ihren Code schreiben, damit der JIT erkennen kann, welche Klasse instanziiert wird. Wenn die Identität der Klasse nicht ermittelt werden kann, kann der Zuordnungscode nicht eingefügt werden. Schlimmer noch, die Escape-Analyse schlägt fehl und das Objekt kann nicht gestapelt werden. Wenn Sie Glück haben, kann die Laufzeitprofilerstellung der JVM Abhilfe schaffen, wenn dieser Code heiß wird, und dynamisch bestimmen, welche Klasse vorherrscht, und für diese optimiert werden.

Beachten Sie, dass die Mikrobenchmarks in diesem Thread stark fehlerhaft sind. Nehmen Sie sie daher mit einem Körnchen Salz. Der mit Abstand am wenigsten fehlerhafte ist der von Peter Lawrey: Er führt Aufwärmläufe durch, um die Methoden zu verbessern, und er besiegt (bewusst) die Fluchtanalyse, um sicherzustellen, dass die Zuweisungen tatsächlich erfolgen. Sogar das hat seine Probleme: Zum Beispiel kann erwartet werden, dass die enorme Anzahl von Array-Speichern Caches und Speicherpuffer besiegt, so dass dies meistens ein Speicher-Benchmark wird, wenn Ihre Zuweisungen sehr schnell sind. (Ein großes Lob an Peter, dass er zu dem Schluss gekommen ist, dass der Unterschied "150 ns" und nicht "2,5x" ist. Ich vermute, dass er so etwas für seinen Lebensunterhalt tut.)

Doradus
quelle
7

Interessanterweise führt die Einstellung von setAccessible (true), bei der die Sicherheitsüberprüfungen übersprungen werden, zu einer Kostenreduzierung von 20%.

Ohne setAccessible (true)

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

Mit setAccessible (true)

new A(), 69 ns
A.class.newInstance(), 159 ns
new A(), 85 ns
A.class.newInstance(), 171 ns
Mikhail Kraizman
quelle
1
Scheint mir im Prinzip offensichtlich. Skalieren diese Zahlen linear, wenn 1000000Aufrufe ausgeführt werden?
Lukas Eder
Tatsächlich setAccessible()kann es im Allgemeinen viel mehr Unterschiede geben, insbesondere bei Methoden mit mehreren Argumenten, daher sollte es immer aufgerufen werden.
StaxMan
6

Ja, es ist deutlich langsamer. Wir haben einen Code ausgeführt, der dies tat, und obwohl mir die Metriken derzeit nicht zur Verfügung stehen, war das Endergebnis, dass wir diesen Code umgestalten mussten, um keine Reflektion zu verwenden. Wenn Sie wissen, was die Klasse ist, rufen Sie einfach den Konstruktor direkt auf.

Elie
quelle
1
+1 Ich habe eine ähnliche Erfahrung gemacht. Es ist gut darauf zu achten, dass Reflexion nur verwendet wird, wenn dies unbedingt erforderlich ist.
Ryan Thames
zB AOP-basierte Bibliotheken müssen reflektiert werden.
Gaurav
4

In doReflection () ist der Overhead aufgrund von Class.forName ("misc.A") (für das eine Klassensuche erforderlich wäre, die möglicherweise den Klassenpfad auf dem Dateisystem scannt) und nicht die für die Klasse aufgerufene newInstance (). Ich frage mich, wie die Statistiken aussehen würden, wenn Class.forName ("misc.A") nur einmal außerhalb der for-Schleife ausgeführt wird. Dies muss nicht unbedingt bei jedem Aufruf der Schleife erfolgen.

Tikoo
quelle
1

Ja, es wird immer langsamer sein, ein Objekt durch Reflektion zu erstellen, da die JVM den Code zur Kompilierungszeit nicht optimieren kann. Weitere Informationen finden Sie in den Sun / Java Reflection-Tutorials .

Siehe diesen einfachen Test:

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
quelle
3
Beachten Sie, dass Sie die Suche ( Class.forName()) von der Instanziierung (newInstance ()) trennen sollten , da sie sich in ihren Leistungsmerkmalen erheblich unterscheiden und Sie die wiederholte Suche in einem gut gestalteten System gelegentlich vermeiden können.
Joachim Sauer
3
Außerdem: Sie müssen jede Aufgabe viele Male ausführen, um einen nützlichen Benchmark zu erhalten: Erstens sind die Aktionen zu langsam, um zuverlässig gemessen zu werden, und zweitens müssen Sie die HotSpot-VM aufwärmen, um nützliche Zahlen zu erhalten.
Joachim Sauer
1

Oft können Sie Apache Commons BeanUtils oder PropertyUtils verwenden, die eine Introspektion durchführen (im Grunde werden die Metadaten über die Klassen zwischengespeichert, sodass sie nicht immer Reflektion verwenden müssen).

Sproketboy
quelle
0

Ich denke, es hängt davon ab, wie leicht / schwer die Zielmethode ist. Wenn die Zielmethode sehr leicht ist (z. B. Getter / Setter), kann sie 1 bis 3 Mal langsamer sein. Wenn die Zielmethode etwa 1 Millisekunde oder mehr dauert, ist die Leistung sehr nahe. Hier ist der Test, den ich mit Java 8 und Reflectasm durchgeführt habe :

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;
        }
    }
}

Der vollständige Testcode ist unter GitHub verfügbar: ReflectionTest.java

user_3380739
quelle