Wydajność odbicia Java


Odpowiedzi:


169

Tak - absolutnie. Patrzenie w górę klasy poprzez refleksję jest według wielkości droższe.

Cytując dokumentację Java na temat refleksji :

Ponieważ odbicie obejmuje typy, które są rozwiązywane dynamicznie, nie można przeprowadzić niektórych optymalizacji maszyn wirtualnych Java. W rezultacie operacje odblaskowe mają wolniejszą wydajność niż ich nieodblaskowe odpowiedniki i należy ich unikać w sekcjach kodu, które są często wywoływane w aplikacjach wrażliwych na wydajność.

Oto prosty test, który zhakowałem w ciągu 5 minut na moim komputerze, używając 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);
    }
}

Z tymi wynikami:

35 // no reflection
465 // using reflection

Należy pamiętać, że wyszukiwanie i tworzenie instancji są wykonywane razem, aw niektórych przypadkach można je ponownie zmienić, ale to tylko podstawowy przykład.

Nawet jeśli tylko utworzysz instancję, nadal otrzymujesz hit wydajnościowy:

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

Ponownie YMMV.


5
Na moim komputerze wywołanie .newInstance () z tylko jednym wywołaniem Class.forName () ma około 30 punktów. W zależności od wersji maszyny wirtualnej różnica może być bliższa niż myślisz przy odpowiedniej strategii buforowania.
Sean Reilly

56
@Peter Lawrey poniżej wskazał, że ten test był całkowicie nieważny, ponieważ kompilator optymalizował rozwiązanie nieodblaskowe (może nawet udowodnić, że nic nie zostało zrobione i zoptymalizować pętlę for). Musi zostać ponownie opracowany i prawdopodobnie powinien zostać usunięty z SO jako złe / wprowadzające w błąd informacje. Buforuj utworzone obiekty w tablicy w obu przypadkach, aby uniemożliwić optymalizatorowi ich optymalizację. (Nie może tego zrobić w sytuacji refleksyjnej, ponieważ nie może udowodnić, że konstruktor nie ma skutków ubocznych)
Bill K

6
@Bill K - nie dajmy się ponieść emocjom. Tak, liczby są wyłączone z powodu optymalizacji. Nie, test nie jest całkowicie nieważny. Dodałem wywołanie, które usuwa jakąkolwiek możliwość wypaczenia wyniku, a liczby nadal są ułożone w stos przed odbiciem. W każdym razie pamiętaj, że jest to bardzo prymitywny mikro-test, który po prostu pokazuje, że refleksja zawsze wiąże się z pewnym narzutem
Yuval Adam

4
To prawdopodobnie bezużyteczny punkt odniesienia. W zależności od tego, co coś robi. Jeśli nie robi nic z widocznym efektem ubocznym, to twój test porównawczy uruchamia tylko martwy kod.
nes1983

9
Właśnie byłem świadkiem 35-krotnej optymalizacji odbicia JVM. Wielokrotne uruchamianie testu w pętli to sposób testowania zoptymalizowanego kodu. Pierwsza iteracja: 3045 ms, druga iteracja: 2941 ms, trzecia iteracja: 90 ms, czwarta iteracja: 83 ms. Kod: c.newInstance (i). c jest konstruktorem. Kod nieodblaskowy: nowy A (i), który daje 13, 4, 3 .. ms razy. Więc tak, refleksja w tym przypadku była powolna, ale nie tak wolna, jak to, co ludzie wyciągają, ponieważ każdy test, który widzę, po prostu uruchamia test raz, nie dając JVM możliwości zastąpienia kodów bajtowych maszyną kod.
Mike

87

Tak, jest wolniejszy.

Ale pamiętajcie o cholernej zasadzie nr 1 - WCZESNA OPTYMALIZACJA JEST KORZENIĄ CAŁEGO ZŁA

(Cóż, może być powiązany z numerem 1 dla DRY)

Przysięgam, gdyby ktoś podszedł do mnie w pracy i zapytał o to, będę bardzo czuwał nad jego kodem przez kilka następnych miesięcy.

Nie możesz nigdy optymalizować, dopóki nie jesteś pewien, że jej potrzebujesz, do tego czasu po prostu napisz dobry, czytelny kod.

Aha, i nie mam na myśli pisania głupiego kodu. Pomyśl tylko o najczystszym sposobie, w jaki możesz to zrobić - bez kopiowania i wklejania itp. (Nadal uważaj na rzeczy takie jak wewnętrzne pętle i korzystaj z kolekcji, która najlepiej pasuje do twoich potrzeb - ignorowanie tego nie jest "niezoptymalizowanym" programowaniem , to „złe” programowanie)

Strasznie mnie przeraża, kiedy słyszę takie pytania, ale potem zapominam, że każdy musi nauczyć się wszystkich zasad, zanim naprawdę je zrozumieją. Otrzymasz go po spędzeniu miesiąca osobowego na debugowaniu czegoś „zoptymalizowanego”.

EDYTOWAĆ:

W tym wątku wydarzyła się ciekawa rzecz. Sprawdź odpowiedź nr 1, to przykład tego, jak potężny jest kompilator w optymalizacji rzeczy. Test jest całkowicie nieważny, ponieważ nieodblaskowa instancja może zostać całkowicie uwzględniona.

Lekcja? NIGDY nie optymalizuj, dopóki nie napiszesz czystego, starannie zakodowanego rozwiązania i nie udowodnisz, że jest zbyt wolne.


28
Całkowicie zgadzam się z sentymentem tej odpowiedzi, jednak jeśli masz zamiar podjąć ważną decyzję projektową, dobrze jest mieć pojęcie o wydajności, aby nie iść na całkowicie niewykonalną ścieżkę. Może po prostu robi należytą staranność?
Układ limbiczny

26
-1: Unikanie robienia rzeczy w niewłaściwy sposób nie jest optymalizacją, to po prostu robienie rzeczy. Optymalizacja to niewłaściwy, skomplikowany sposób z powodu rzeczywistych lub wyimaginowanych problemów z wydajnością.
soru

5
@soru całkowicie się zgadzam. Wybór połączonej listy zamiast listy tablic do sortowania przez wstawianie jest po prostu właściwym sposobem robienia rzeczy. Ale to konkretne pytanie - istnieją dobre przypadki użycia dla obu stron pierwotnego pytania, więc wybranie takiego opartego na wydajności, a nie na najbardziej użytecznym rozwiązaniu byłoby niewłaściwe. Nie jestem pewien, czy w ogóle się nie zgadzamy, więc nie jestem pewien, dlaczego powiedziałeś „-1”.
Bill K

14
Każdy rozsądny programista-analityk musi wziąć pod uwagę wydajność na wczesnym etapie, w przeciwnym razie możesz otrzymać system, którego NIE można zoptymalizować w wydajnym i opłacalnym okresie. Nie, nie optymalizujesz każdego cyklu zegara, ale z pewnością stosujesz najlepsze praktyki do czegoś tak podstawowego, jak tworzenie instancji klasy. Ten przykład jest świetnym przykładem DLACZEGO rozważasz takie pytania dotyczące refleksji. Byłby to raczej kiepski programista, który poszedł naprzód i użył odbicia w milionowym systemie tylko po to, by później odkryć, że był on o rząd wielkości zbyt wolny.
RichieHH

2
@Richard Riley Generalnie tworzenie instancji klas jest dość rzadkim zdarzeniem dla wybranych klas, nad którymi będziesz używać refleksji. Przypuszczam jednak, że masz rację - niektórzy ludzie mogą tworzyć instancje każdej klasy w sposób refleksyjny, nawet te, które są stale odtwarzane. Nazwałbym to dość złym programowaniem (chociaż nawet wtedy MOŻESZ zaimplementować pamięć podręczną instancji klas do ponownego użycia po fakcie i nie zaszkodzić zbytnio kodowi - więc myślę, że nadal powiedziałbym ZAWSZE projektuj pod kątem czytelności, a następnie profiluj i optymalizuj później)
Bill K

36

Może się okazać, że A a = new A () jest optymalizowane przez JVM. Jeśli umieścisz obiekty w tablicy, nie będą one działać tak dobrze. ;) Poniższe wydruki ...

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

To sugeruje, że różnica na moim komputerze wynosi około 150 ns.


więc właśnie zabiłeś optymalizator, więc teraz obie wersje są wolne. Dlatego refleksja jest nadal cholernie powolna.
gbjbaanb

13
@gbjbaanb Jeśli optymalizator optymalizował samą kreację, to nie był to prawidłowy test. Test @ Petera jest zatem ważny, ponieważ faktycznie porównuje czasy tworzenia (optymalizator nie byłby w stanie działać w ŻADNEJ rzeczywistej sytuacji, ponieważ w każdej rzeczywistej sytuacji potrzebujesz obiektów, które tworzysz).
Bill K

10
@ nes1983 W takim przypadku mógłbyś skorzystać z okazji, aby stworzyć lepszy wzorzec. Być może możesz zaoferować coś konstruktywnego, na przykład to, co powinno znajdować się w treści metody.
Peter Lawrey

1
na moim Macu, openjdk 7u4, różnica wynosi 95ns w porównaniu do 100ns. Zamiast przechowywać A w tablicy, przechowuję hashCodes. Jeśli powiesz -verbose: class, możesz zobaczyć, kiedy hotspot generuje kod bajtowy do konstruowania A i towarzyszącego mu przyspieszenia.
Ron

@PeterLawrey Jeśli wyszukam raz (jedno połączenie Class.getDeclaredMethod), a potem zadzwonię Method.invokewiele razy? Czy używam odbicia raz, czy tyle razy, ile go przywołuję? Pytanie uzupełniające, co jeśli zamiast Methodtego jest a, Constructora ja robię Constructor.newInstancewiele razy?
tmj

28

Jeśli naprawdę istnieje potrzeba czegoś szybszego niż odbicie i nie jest to tylko przedwczesna optymalizacja, to generowanie kodu bajtowego z ASM to opcją jest lub biblioteką wyższego poziomu. Generowanie kodu bajtowego za pierwszym razem jest wolniejsze niż zwykłe użycie odbicia, ale po wygenerowaniu kodu bajtowego jest tak szybkie, jak normalny kod Java i zostanie zoptymalizowany przez kompilator JIT.

Kilka przykładów aplikacji wykorzystujących generowanie kodu:

  • Wywoływanie metod na serwerach proxy generowanych przez CGLIB jest nieco szybsze niż dynamiczne serwery proxy Javy , ponieważ CGLIB generuje kod bajtowy dla swoich serwerów proxy, ale dynamiczne proxy używają tylko odbicia ( zmierzyłem, że CGLIB jest około 10 razy szybszy w wywołaniach metod, ale tworzenie proxy było wolniejsze).

  • JSerial generuje kod bajtowy do odczytu / zapisu pól obiektów serializowanych, zamiast używać odbicia. Na stronie JSerial jest kilka testów porównawczych .

  • Nie jestem pewien w 100% (i nie mam teraz ochoty czytać źródła), ale myślę, że Guice generuje kod bajtowy do wstrzykiwania zależności. Popraw mnie, jeśli się mylę.


27

„Znaczące” jest całkowicie zależne od kontekstu.

Jeśli używasz odbicia do tworzenia pojedynczego obiektu obsługi na podstawie jakiegoś pliku konfiguracyjnego, a następnie spędzasz resztę czasu na wykonywaniu zapytań do bazy danych, to jest to nieistotne. Jeśli tworzysz dużą liczbę obiektów poprzez odbicie w ciasnej pętli, to tak, to jest ważne.

Ogólnie rzecz biorąc, elastyczność projektowania (w razie potrzeby!) Powinna wpływać na refleksję, a nie wydajność. Jednak aby określić, czy wydajność jest problemem, należy raczej profilować, niż uzyskiwać arbitralne odpowiedzi z forum dyskusyjnego.


24

Istnieje trochę narzutów z odbiciem, ale na nowoczesnych maszynach wirtualnych jest on znacznie mniejszy niż kiedyś.

Jeśli używasz odbicia do tworzenia każdego prostego obiektu w swoim programie, coś jest nie tak. Używanie go okazjonalnie, gdy masz dobry powód, nie powinno w ogóle stanowić problemu.


11

Tak, podczas korzystania z odbicia występuje spadek wydajności, ale możliwym obejściem optymalizacji jest buforowanie metody:

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

spowoduje:

[java] Odruchowe wywołanie metody 1000000 razy z wyszukiwaniem zajęło 5618 milisów

[java] Odruchowe wywołanie metody 1000000 razy z pamięcią podręczną zajęło 270 milisekund


Ponowne użycie metody / konstruktora jest rzeczywiście przydatne i pomaga, ale należy zauważyć, że powyższy test nie daje miarodajnych liczb ze względu na typowe problemy z testami porównawczymi (brak rozgrzewki, więc w szczególności pierwsza pętla mierzy czas rozgrzewania JVM / JIT).
StaxMan

7

Odbicie jest powolne, chociaż alokacja obiektów nie jest tak beznadziejna, jak inne aspekty refleksji. Osiągnięcie równoważnej wydajności z tworzeniem instancji opartej na odbiciu wymaga napisania kodu, aby jit mógł określić, która klasa jest tworzona. Jeśli nie można określić tożsamości klasy, nie można wstawić kodu alokacji. Co gorsza, analiza ucieczki kończy się niepowodzeniem, a obiektu nie można zaalokować na stosie. Jeśli masz szczęście, profilowanie w czasie wykonywania maszyny JVM może przyjść na ratunek, jeśli ten kod stanie się gorący i może dynamicznie określić, która klasa dominuje i może zostać zoptymalizowana pod kątem tej.

Należy pamiętać, że mikrozlamy w tym wątku są głęboko wadliwe, więc weź je z przymrużeniem oka. Najmniej wadliwy jest zdecydowanie Peter Lawrey: wykonuje rozgrzewki, aby zrzucić metody, i (świadomie) pokonuje analizę ucieczki, aby upewnić się, że alokacje faktycznie mają miejsce. Nawet to ma jednak swoje problemy: na przykład można oczekiwać, że ogromna liczba magazynów macierzy pokona pamięci podręczne i bufory przechowywania, więc będzie to głównie test porównawczy pamięci, jeśli alokacje są bardzo szybkie. (Chwała Peterowi za prawidłowe wyciągnięcie wniosku: różnica wynosi „150 ns”, a nie „2,5x”. Podejrzewam, że zajmuje się takimi rzeczami na życie).


7

Co ciekawe, ustawienie setAccessible (true), które pomija kontrole bezpieczeństwa, daje 20% redukcję kosztów.

Bez setAccessible (true)

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

Z setAccessible (true)

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

1
W zasadzie wydaje mi się to oczywiste. Czy te liczby skalują się liniowo podczas uruchamiania 1000000wywołań?
Lukas Eder

W rzeczywistości setAccessible()może mieć znacznie większą różnicę w ogóle, szczególnie w przypadku metod z wieloma argumentami, więc zawsze należy ją wywołać.
StaxMan

6

Tak, jest znacznie wolniejszy. Uruchomiliśmy kod, który to zrobił i chociaż w tej chwili nie mam dostępnych metryk, efekt końcowy był taki, że musieliśmy refaktoryzować ten kod, aby nie używać odbicia. Jeśli wiesz, czym jest klasa, po prostu zadzwoń bezpośrednio do konstruktora.


1
+1 Miałem podobne doświadczenie. Dobrze jest korzystać z odbicia tylko wtedy, gdy jest to absolutnie konieczne.
Ryan Thames

np. biblioteki oparte na AOP wymagają refleksji.
gaurav

4

W doReflection () jest narzut spowodowany Class.forName („misc.A”) (który wymagałby przeszukania klasy, potencjalnie skanując ścieżkę klasy w systemie plików), a nie newInstance () wywołanej na klasie. Zastanawiam się, jak wyglądałyby statystyki, gdyby Class.forName ("misc.A") było wykonywane tylko raz poza pętlą for, tak naprawdę nie trzeba tego robić przy każdym wywołaniu pętli.


1

Tak, zawsze wolniej będzie tworzyć obiekt przez odbicie, ponieważ JVM nie może zoptymalizować kodu w czasie kompilacji. Więcej informacji można znaleźć w samouczkach dotyczących refleksji Sun / Java .

Zobacz ten prosty 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
Zwróć uwagę, że powinieneś oddzielić Class.forName()metodę lookup ( ) od instancjacji (newInstance ()), ponieważ różnią się one znacznie w charakterystyce wydajności i czasami możesz uniknąć wielokrotnego wyszukiwania w dobrze zaprojektowanym systemie.
Joachim Sauer

3
Ponadto: musisz wykonać każde zadanie wiele, wiele razy, aby uzyskać przydatny test porównawczy: po pierwsze działania są zbyt wolne, aby można je było wiarygodnie zmierzyć, a po drugie, aby uzyskać przydatne liczby, musisz rozgrzać maszynę wirtualną HotSpot.
Joachim Sauer

1

Często możesz użyć Apache commons BeanUtils lub PropertyUtils, które przeprowadzają introspekcję (w zasadzie buforują metadane o klasach, więc nie zawsze muszą używać odbicia).


0

Myślę, że zależy to od tego, jak lekka / ciężka jest metoda docelowa. jeśli metoda docelowa jest bardzo lekka (np. getter / setter), może być 1 ~ 3 razy wolniejsza. jeśli metoda docelowa zajmie około 1 milisekundy lub więcej, wydajność będzie bardzo zbliżona. oto test, który przeprowadziłem z Javą 8 i reflektazmem :

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

Pełny kod testu jest dostępny na GitHub: ReflectionTest.java

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.