Prestazioni Java Reflection


Risposte:


169

Si assolutamente. Cercare una classe tramite la riflessione è, per grandezza , più costoso.

Citando la documentazione di Java sulla riflessione :

Poiché la riflessione implica tipi che vengono risolti dinamicamente, alcune ottimizzazioni della macchina virtuale Java non possono essere eseguite. Di conseguenza, le operazioni riflettenti hanno prestazioni più lente rispetto alle loro controparti non riflettenti e dovrebbero essere evitate in sezioni di codice che vengono chiamate frequentemente in applicazioni sensibili alle prestazioni.

Ecco un semplice test che ho hackerato in 5 minuti sulla mia macchina, eseguendo 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);
    }
}

Con questi risultati:

35 // no reflection
465 // using reflection

Tieni presente che la ricerca e l'istanza vengono eseguite insieme e in alcuni casi la ricerca può essere rifattorizzata, ma questo è solo un esempio di base.

Anche se hai solo un'istanza, ottieni comunque un successo in termini di prestazioni:

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

Ancora una volta, YMMV.


5
Sulla mia macchina la chiamata .newInstance () con una sola chiamata Class.forName () ha un punteggio di circa 30. A seconda della versione della VM, la differenza potrebbe essere più vicina di quanto si pensi con una strategia di memorizzazione nella cache appropriata.
Sean Reilly,

56
@Peter Lawrey di seguito ha sottolineato che questo test era completamente non valido perché il compilatore stava ottimizzando la soluzione non riflettente (può persino dimostrare che non viene fatto nulla e ottimizzare il ciclo for). Deve essere rielaborato e probabilmente dovrebbe essere rimosso da SO come informazione errata / fuorviante. Cache gli oggetti creati in un array in entrambi i casi per evitare che l'ottimizzatore lo ottimizzi. (Non può farlo nella situazione riflessiva perché non può dimostrare che il costruttore non abbia effetti collaterali)
Bill K,

6
@ Bill K - non lasciamoci trasportare. Sì, i numeri sono disattivati ​​a causa di ottimizzazioni. No, il test non è completamente non valido. Ho aggiunto una chiamata che rimuove qualsiasi possibilità di distorcere il risultato e i numeri sono ancora impilati contro la riflessione. In ogni caso, ricorda che questo è un micro-benchmark molto rozzo che mostra solo che la riflessione comporta sempre un certo sovraccarico
Yuval Adam,

4
Questo è probabilmente un punto di riferimento inutile. A seconda di cosa fa qualcosa. Se non fa nulla con effetti collaterali visibili, il tuo benchmark esegue solo codice morto.
nes1983,

9
Ho appena visto la JVM ottimizzare la riflessione 35 volte. L'esecuzione ripetuta del test in un ciclo è il modo in cui testare il codice ottimizzato. Prima iterazione: 3045 ms, seconda iterazione: 2941 ms, terza iterazione: 90 ms, quarta iterazione: 83 ms. Codice: c.newInstance (i). c è un costruttore. Codice non riflettente: nuovo A (i), che produce 13, 4, 3 .. ms volte. Quindi sì, la riflessione in questo caso è stata lenta, ma non tanto più lenta di quanto stanno concludendo le persone, perché ogni test che vedo, lo eseguono semplicemente una volta senza dare alla JVM l'opportunità di sostituire i codici byte con la macchina codice.
Mike,

87

Sì, è più lento.

Ma ricorda la dannata regola n. 1: L'OTTIMIZZAZIONE DELLE PREMATURE È LA RADICE DI TUTTO IL MALE

(Beh, potrebbe essere legato con il n. 1 per DRY)

Lo giuro, se qualcuno venisse da me al lavoro e mi chiedesse questo sarei molto attento al loro codice per i prossimi mesi.

Non devi mai ottimizzare fino a quando non sei sicuro di averne bisogno, fino ad allora, basta scrivere un buon codice leggibile.

Oh, e non intendo nemmeno scrivere codice stupido. Pensa solo al modo più pulito in cui puoi farlo - nessuna copia e incolla, ecc. (Fai ancora attenzione a cose come i loop interni e usa la collezione che si adatta meglio alle tue esigenze - Ignorare queste non è una programmazione "non ottimizzata" , è "cattiva" programmazione)

Mi fa impazzire quando sento domande del genere, ma poi dimentico che tutti devono imparare da soli tutte le regole prima di capirlo davvero. Lo otterrai dopo aver trascorso un mese a fare il debug di qualcosa di "ottimizzato".

MODIFICARE:

In questo thread è successa una cosa interessante. Controlla la risposta n. 1, è un esempio di quanto sia potente il compilatore nell'ottimizzare le cose. Il test non è completamente valido poiché l'istanza non riflettente può essere completamente fattorizzata.

Lezione? Non ottimizzare MAI fino a quando non hai scritto una soluzione pulita, ben codificata e provata che è troppo lenta.


28
Sono totalmente d'accordo con il sentimento di questa risposta, tuttavia se stai per intraprendere un'importante decisione di progettazione, ti sarà di aiuto avere un'idea delle prestazioni in modo da non intraprendere un percorso totalmente impraticabile. Forse sta solo facendo la dovuta diligenza?
Sistema limbico,

26
-1: Evitare di fare le cose nel modo sbagliato non è l'ottimizzazione, è solo fare le cose. L'ottimizzazione sta facendo le cose nel modo sbagliato e complicato a causa di preoccupazioni prestazionali reali o immaginarie.
soru,

5
@soru è totalmente d'accordo. Scegliere un elenco collegato su un elenco di array per un ordinamento di inserzione è semplicemente il modo giusto di fare le cose. Ma questa domanda particolare - ci sono buoni casi d'uso per entrambi i lati della domanda originale, quindi scegliere uno basato sulle prestazioni piuttosto che sulla soluzione più utilizzabile sarebbe sbagliato. Non sono sicuro che non siamo affatto d'accordo, quindi non sono sicuro del motivo per cui hai detto "-1".
Bill K,

14
Qualunque programmatore di analista sensibile deve considerare l'efficienza in una fase precoce o si potrebbe finire con un sistema che NON può essere ottimizzato in un lasso di tempo efficiente e conveniente. No, non ottimizzi tutti i cicli di clock ma sicuramente utilizzi le migliori pratiche per qualcosa di basilare come l'istanza di classe. Questo esempio è grandioso del PERCHÉ consideri tali domande riguardo alla riflessione. Sarebbe stato un programmatore piuttosto povero che sarebbe andato avanti e avrebbe usato la riflessione attraverso un sistema di milioni di linee solo per scoprire in seguito che gli ordini di grandezza erano troppo lenti.
RichieHH,

2
@Richard Riley Generalmente l'istanza di classe è un evento piuttosto raro per le classi selezionate su cui userete la riflessione. Suppongo che tu abbia ragione: alcune persone potrebbero creare un'istanza di ogni classe in modo riflessivo, anche quelle che vengono ricreate costantemente. Definirei una programmazione piuttosto scadente (anche se anche allora POTREBBE implementare una cache di istanze di classe per il riutilizzo dopo il fatto e non danneggiare troppo il tuo codice - quindi suppongo che direi SEMPRE progettazione SEMPRE per leggibilità, quindi profilo e ottimizzazione più tardi)
Bill K,

36

È possibile che A a = new A () venga ottimizzato dalla JVM. Se metti gli oggetti in un array, non funzionano così bene. ;) Le seguenti stampe ...

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

Questo suggerisce che la differenza è di circa 150 ns sulla mia macchina.


quindi hai appena ucciso l'ottimizzatore, quindi ora entrambe le versioni sono lente. La riflessione è quindi dannatamente lenta.
gbjbaanb,

13
@gbjbaanb se l'ottimizzatore stava ottimizzando la creazione stessa, allora non era un test valido. Il test di @ Peter è quindi valido perché confronta effettivamente i tempi di creazione (l'ottimizzatore non sarebbe in grado di funzionare in QUALSIASI situazione del mondo reale perché in qualsiasi situazione del mondo reale hai bisogno degli oggetti che stai istanziando).
Bill K,

10
@ nes1983 Nel qual caso avresti potuto cogliere l'occasione per creare un benchmark migliore. Forse puoi offrire qualcosa di costruttivo, come quello che dovrebbe essere nel corpo del metodo.
Peter Lawrey,

1
sul mio mac, openjdk 7u4, la differenza è 95 ns contro 100 ns. Invece di archiviare A nell'array, memorizzo hashCodes. Se dici -verbose: class puoi vedere quando un hotspot genera un bytecode per costruire A e lo speedup di accompagnamento.
Ron,

@PeterLawrey Se cerco una volta (una chiamata a Class.getDeclaredMethod) e poi chiamo Method.invokepiù volte? Sto usando il riflesso una volta o tutte le volte che lo invoco? Domanda di follow-up, cosa succede se invece di Methodè un Constructore lo faccio Constructor.newInstancepiù volte?
tmj

28

Se c'è davvero bisogno di qualcosa di più veloce della riflessione, e non è solo un'ottimizzazione prematura, allora generazione di bytecode con ASM o una libreria di livello superiore è un'opzione. La generazione del bytecode la prima volta è più lenta rispetto al semplice utilizzo di reflection, ma una volta generato il bytecode, è veloce come il normale codice Java e sarà ottimizzato dal compilatore JIT.

Alcuni esempi di applicazioni che utilizzano la generazione di codice:

  • Invocare metodi su proxy generati da CGLIB è leggermente più veloce dei proxy dinamici di Java , perché CGLIB genera bytecode per i suoi proxy, ma i proxy dinamici usano solo la riflessione ( ho misurato CGLIB per essere circa 10 volte più veloce nelle chiamate di metodo, ma la creazione dei proxy è stata più lenta).

  • JSerial genera un bytecode per leggere / scrivere i campi di oggetti serializzati, invece di usare la riflessione. Ci sono alcuni benchmark sul sito di JSerial.

  • Non sono sicuro al 100% (e non mi va di leggere la fonte ora), ma penso che Guice generi bytecode per fare l'iniezione di dipendenza. Correggimi se sbaglio.


27

"Significativo" dipende interamente dal contesto.

Se stai usando reflection per creare un singolo oggetto gestore basato su alcuni file di configurazione e poi trascorri il resto del tempo a eseguire query sul database, allora è insignificante. Se stai creando un gran numero di oggetti tramite la riflessione in un ciclo stretto, quindi sì, è significativo.

In generale, la flessibilità del design (dove necessario!) Dovrebbe guidare il tuo uso della riflessione, non delle prestazioni. Tuttavia, per determinare se le prestazioni sono un problema, è necessario creare un profilo anziché ottenere risposte arbitrarie da un forum di discussione.


24

C'è un certo sovraccarico con la riflessione, ma è molto più piccolo nelle VM moderne di quanto non fosse in passato.

Se stai usando la riflessione per creare ogni semplice oggetto nel tuo programma, allora qualcosa non va. Usarlo di tanto in tanto, quando hai buone ragioni, non dovrebbe essere affatto un problema.


11

Sì, c'è un impatto sulle prestazioni quando si utilizza Reflection, ma una possibile soluzione per l'ottimizzazione è la memorizzazione nella cache del metodo:

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

comporterà:

[java] Il metodo di chiamata 1000000 volte riflessivo con la ricerca ha richiesto 5618 millis

[java] Il metodo di chiamata 1000000 volte riflessivo con cache ha richiesto 270 millis


Riutilizzare il metodo / costruttore è davvero utile e aiuta, ma nota che il test sopra non fornisce numeri significativi a causa dei soliti problemi di benchmarking (nessun riscaldamento, quindi il primo ciclo in particolare sta misurando principalmente il tempo di riscaldamento JVM / JIT).
StaxMan,

7

La riflessione è lenta, sebbene l'allocazione degli oggetti non sia così disperata come altri aspetti della riflessione. Per ottenere prestazioni equivalenti con un'istanza basata sulla riflessione è necessario scrivere il codice in modo che jit possa dire quale classe viene istanziata. Se non è possibile determinare l'identità della classe, non è possibile incorporare il codice di allocazione. Peggio ancora, l'analisi di escape ha esito negativo e l'oggetto non può essere allocato in pila. Se sei fortunato, la profilazione di runtime della JVM può venire in soccorso se questo codice si surriscalda e può determinare dinamicamente quale classe predomina e può ottimizzare per quella.

Ricorda che i microbench in questo filo sono profondamente imperfetti, quindi prendili con un granello di sale. Il meno imperfetto è di gran lunga quello di Peter Lawrey: esegue i riscaldamenti per ottenere i metodi corretti e sconfigge (consapevolmente) l'analisi di fuga per garantire che le allocazioni si stiano effettivamente verificando. Anche se uno ha i suoi problemi, però: ad esempio, ci si può aspettare che l'enorme numero di negozi di array sconfigga le cache e memorizzi i buffer, quindi si tratterà principalmente di un benchmark di memoria se le allocazioni sono molto veloci. (Complimenti a Peter per ottenere la conclusione giusta però: che la differenza è "150ns" piuttosto che "2.5x". Sospetto che faccia questo genere di cose per vivere.)


7

È interessante notare che l'impostazione di setAccessible (true), che salta i controlli di sicurezza, ha una riduzione dei costi del 20%.

Senza setAccessible (true)

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

Con setAccessible (true)

new A(), 69 ns
A.class.newInstance(), 159 ns
new A(), 85 ns
A.class.newInstance(), 171 ns

1
Mi sembra ovvio in linea di principio. Questi numeri si ridimensionano linearmente quando si eseguono 1000000invocazioni?
Lukas Eder,

In realtà setAccessible()può avere molta più differenza in generale, specialmente per metodi con argomenti multipli, quindi dovrebbe sempre essere chiamato.
StaxMan,

6

Sì, è significativamente più lento. Stavamo eseguendo un po 'di codice per farlo, e mentre al momento non ho le metriche disponibili, il risultato finale è stato che abbiamo dovuto refactoring quel codice per non usare la riflessione. Se sai qual è la classe, chiama direttamente il costruttore.


1
+1 Ho avuto un'esperienza simile. È bene assicurarsi di utilizzare la riflessione solo se è assolutamente necessario.
Ryan Thames,

ad es. le librerie basate su AOP necessitano di riflessione.
gaurav,

4

Nel doReflection () c'è l'overhead a causa di Class.forName ("misc.A") (che richiederebbe una ricerca di classe, potenzialmente scansionando il percorso della classe sul filsystem), piuttosto che il newInstance () chiamato sulla classe. Mi chiedo come sarebbero le statistiche se Class.forName ("misc.A") viene eseguito solo una volta al di fuori del for-loop, non è necessario farlo per ogni invocazione del loop.


1

Sì, sarà sempre più lento creare un oggetto riflesso perché la JVM non può ottimizzare il codice al momento della compilazione. Vedere i tutorial di Sun / Java Reflection per maggiori dettagli.

Vedi questo semplice 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");
    }
}

3
Si noti che è necessario separare la ricerca ( Class.forName()) dall'istanza (newInstance ()), perché variano in modo significativo nelle loro caratteristiche prestazionali e occasionalmente è possibile evitare la ricerca ripetuta in un sistema ben progettato.
Joachim Sauer,

3
Inoltre: devi eseguire ogni attività molte, molte volte per ottenere un utile benchmark: prima di tutto le azioni sono troppo lente per essere misurate in modo affidabile e in secondo luogo dovrai riscaldare la VM HotSpot per ottenere numeri utili.
Joachim Sauer,

1

Spesso puoi usare i comuni di Apache BeanUtils o PropertyUtils quale introspezione (in pratica memorizzano nella cache i metadati sulle classi in modo che non debbano sempre usare la riflessione).


0

Penso che dipenda da quanto sia leggero / pesante il metodo target. se il metodo target è molto leggero (ad es. getter / setter), potrebbe essere 1 ~ 3 volte più lento. se il metodo target impiega circa 1 millisecondo o superiore, le prestazioni saranno molto vicine. ecco il test che ho fatto con Java 8 e reflectionasm :

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

Il codice di prova completo è disponibile su GitHub: ReflectionTest.java

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.