Продуктивність відбиття Java


172

Чи створює об'єкт за допомогою рефлексії, а не викликає конструктор класу, якісь значні відмінності у продуктивності?


Відповіді:


169

Так - абсолютно. Пошук класу за допомогою відображення за величиною дорожче.

Цитування документації Java про відображення :

Оскільки рефлексія включає типи, які динамічно вирішуються, певні оптимізації віртуальної машини Java не можуть бути виконані. Отже, світловідбиваючі операції мають меншу продуктивність, ніж їх невідбиваючі аналоги, і їх слід уникати у розділах коду, які часто називають у чутливих до продуктивності додатках.

Ось простий тест, який я зламав за 5 хвилин на своїй машині, запускаючи 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);
    }
}

З цими результатами:

35 // no reflection
465 // using reflection

Майте на увазі, що пошук і інстанції робляться разом, а в деяких випадках пошук можна відновлювати, але це лише основний приклад.

Навіть якщо ви просто подали копію, ви все одно отримаєте хіт продуктивності:

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

Знову ж, YMMV.


5
На моїй машині дзвінок .newInstance (), який має лише один дзвінок Class.forName (), складає 30 або більше. Залежно від версії VM, різниця може бути ближчою, ніж ви думаєте, відповідною стратегією кешування.
Шон Рейлі

56
@ Петер Лоурі нижче вказав, що цей тест був абсолютно недійсним, оскільки компілятор оптимізував невідбивне рішення (Це навіть може довести, що нічого не робиться, і оптимізувати цикл для циклу). Необхідно переробити, і, ймовірно, слід вилучити з ПЗ як погану / оманливу інформацію. Кешуйте створені об'єкти в масиві в обох випадках, щоб запобігти оптимізатору оптимізувати його. (Це не може зробити це у рефлексивній ситуації, оскільки не може довести, що у конструктора немає побічних ефектів)
Білл К

6
@Bill K - не будемо захоплюватися. Так, номери вимкнено через оптимізацію. Ні, тест не є абсолютно недійсним. Я додав дзвінок, який видаляє будь-яку можливість перекосу результату, і номери все ще зберігаються проти відображення. У будь-якому випадку, пам’ятайте, що це дуже сирий мікро-орієнтир, який просто показує, що відображення завжди несе певні накладні витрати
Юваль Адам,

4
Це, мабуть, марний орієнтир. Залежно від того, що робить щось. Якщо з видимим побічним ефектом нічого не відбувається, то ваш бенчмарк виконує лише мертвий код.
nes1983

9
Я щойно став свідком оптимізації відбиття JVM у 35 разів. Запуск тесту повторно в циклі - це те, як ви тестуєте оптимізований код. Перша ітерація: 3045 мс, друга ітерація: 2941 мс, третя ітерація: 90 мс, четверта ітерація: 83 мс. Код: c.newInstance (i). c - Конструктор. Невідбиваючий код: новий A (i), який дає 13, 4, 3 .. мс разів. Так, так, рефлексія в цьому випадку була повільною, але майже не настільки повільною, як те, що люди роблять, тому що кожен тест, який я бачу, вони просто проводять тест один раз, не даючи JVM можливості замінити байт-коди машинними код.
Майк

87

Так, це повільніше.

Але пам’ятайте прокляте №1 правило - ОПТИМІЗАЦІЯ ПРЕМАТУРИ - це корінь усього злого

(Що ж, може бути пов'язано з №1 для DRY)

Я клянусь, якби хтось підійшов до мене на роботі і запитав у мене це, я б дуже насторожився над їх кодом протягом наступних кількох місяців.

Ніколи не слід оптимізувати, поки ви впевнені, що вам це потрібно, до цього часу просто напишіть хороший читабельний код.

О, і я не маю на увазі також написати дурний код. Подумайте лише про найчистіший спосіб, яким ви це можете зробити - без копіювання та вставки тощо (все ж будьте обережні до таких речей, як внутрішні петлі та використання колекції, що найкраще відповідає вашим потребам. Ігнорування цих не "неоптимізованих" програмувань , це "погано" програмування)

Мене це лякає, коли я чую подібні запитання, але тоді я забуваю, що всі повинні самі вивчити всі правила, перш ніж вони дійсно їх отримають. Ви отримаєте це після того, як витратили місячний місяць на налагодження чогось, кого хтось "оптимізував".

Редагувати:

Цікава річ трапилась у цій темі. Перевірте відповідь №1, це приклад того, наскільки потужний компілятор в оптимізації речей. Тест є абсолютно недійсним, оскільки невідбиваючу інстанцію можна повністю врахувати.

Урок? НІКОЛИ не оптимізуйте, поки ви не написали чисте, акуратно розроблене рішення і не довели, що це занадто повільно.


28
Я повністю згоден з настроєм цієї відповіді, однак, якщо ви збираєтеся прийняти важливе дизайнерське рішення, це допоможе скласти уявлення про продуктивність, щоб ви не відходили на абсолютно непрацездатний шлях. Може, він просто займається належною ретельністю?
Лімбічна система

26
-1: Уникати того, щоб робити речі неправильно, це не оптимізація, це просто робити речі. Оптимізація робить речі неправильним, складним способом через реальну чи уявну загрозу ефективності.
сор

5
@soru повністю згоден. Вибір зв'язаного списку над списком масиву для сортування вставки - це просто правильний спосіб робити. Але саме це питання - існують хороші випадки використання для обох сторін оригінального питання, тому вибір одного на основі продуктивності, а не найбільш корисного рішення був би неправильним. Я не впевнений, що ми взагалі не погоджуємось, тому я не впевнений, чому ви сказали "-1".
Білл К

14
Будь-який розумний програміст-аналітик повинен врахувати ефективність на ранній стадії, або ви можете отримати систему, яку НЕ МОЖЕ бути оптимізовано в ефективні та достойні часові рамки. Ні, ви не оптимізуєте кожен тактовий цикл, але ви, звичайно, використовуєте найкращі практики для чогось такого базового, як інстанція класу. Цей приклад чудовий, ЧОМУ ви розглядаєте такі питання щодо роздумів. Це був би досить бідний програміст, який пішов би вперед і використовував роздуми по всій системі мільйонів ліній, щоб згодом виявити, що це порядки занадто повільно.
RichieHH

2
@Richard Riley Загалом, опис класів - це досить рідкісна подія для вибраних класів, про які ви будете використовувати роздуми. Я гадаю, що ти маєш рацію - деякі люди можуть відображати кожен клас рефлексійно, навіть ті, які відтворюються постійно. Я б назвав це досить поганим програмуванням (хоча навіть тоді ви МОЖЕ впровадити кеш екземплярів класу для повторного використання після факту і не зашкодити вашому коду занадто сильно - тому, мабуть, я все-таки скажу ЗАВЖДИ дизайн для читабельності, потім профіль та оптимізація пізніше)
Білл К

36

Ви можете виявити, що A a = new A () оптимізується JVM. Якщо ви помістите об’єкти в масив, вони не так добре працюють. ;) Наступні відбитки ...

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

Це дозволяє припустити, що різниця становить приблизно 150 нс на моїй машині.


так що ви просто вбили оптимізатора, тому тепер обидві версії повільні. Рефлексія, таким чином, все ще проклята повільно.
gbjbaanb

13
@gbjbaanb, якщо оптимізатор оптимізував саме створення, то це не був коректним тестом. @ Тест Петра є дійсним, тому що він фактично порівнює часи створення (Оптимізатор не міг би працювати в будь-якій ситуації в реальному світі, тому що в будь-якій ситуації в реальному світі вам потрібні об'єкти, які ви інтенчуєте).
Білл К

10
@ nes1983 У цьому випадку ви могли скористатися можливістю створити кращий орієнтир. Можливо, ви можете запропонувати щось конструктивне, як, наприклад, те, що має бути в тілі методу.
Пітер Лоурі

1
на моєму mac, openjdk 7u4, різниця - 95ns проти 100ns. Замість того, щоб зберігати A у масиві, я зберігаю hashCodes. Якщо ви скажете -verbose: клас, ви можете бачити, коли точка доступу генерує байт-код для побудови A та супутнього прискорення.
Рон

@PeterLawrey Якщо я шукаю один раз (один дзвінок Class.getDeclaredMethod), а потім дзвоню Method.invokeкілька разів? Чи використовую я рефлексію раз чи стільки разів, скільки викликаю це? Подальше запитання, що робити, якщо замість Methodцього є Constructorі я роблю Constructor.newInstanceкілька разів?
tmj

28

Якщо дійсно потрібно щось швидше, ніж рефлексія, і це не просто передчасна оптимізація, то генерація байтових кодів за допомогою ASM або бібліотеки вищого рівня - це варіант. Генерування байт-коду вперше відбувається повільніше, ніж просто використання відображення, але як тільки байт-код був сформований, він буде таким же швидким, як звичайний код Java, і його оптимізує компілятор JIT.

Деякі приклади програм, які використовують генерацію коду:

  • Методи виклику проксі-серверів, що генеруються CGLIB , трохи швидші, ніж динамічні проксі-сервери Java , оскільки CGLIB генерує байт-код для своїх проксі, але динамічні проксі-сервери використовують лише відображення ( я вимірював CGLIB на 10 разів швидше при викликах методів, але створення проксі-серверів було повільніше).

  • JSerial генерує байт-код для читання / запису полів серіалізованих об'єктів, замість використання рефлексії. На сайті JSerial є деякі орієнтири .

  • Я не на 100% впевнений (і зараз мені не хочеться читати джерело), ​​але я думаю, що Гійс генерує байт-код, щоб зробити ін'єкцію залежності. Виправте мене, якщо я помиляюся.


27

"Значуще" повністю залежить від контексту.

Якщо ви використовуєте відображення для створення єдиного оброблювального об'єкта на основі деякого файлу конфігурації, а потім витрачаєте решту часу на виконання запитів до бази даних, то це несуттєво. Якщо ви створюєте велику кількість об'єктів за допомогою відображення в тісному циклі, то так, це важливо.

Взагалі, гнучкість дизайну (де це потрібно!) Повинна керувати вашим використанням відображення, а не продуктивності. Однак, щоб визначити, чи є проблема в роботі, потрібно проаналізувати, а не отримати довільні відповіді з дискусійного форуму.


24

Існує деяка накладні витрати з відображенням, але вона набагато менша, ніж у сучасних ВМ, ніж раніше.

Якщо ви використовуєте роздуми для створення кожного простого об'єкта у вашій програмі, то щось не так. Використовувати його час від часу, коли у вас є вагомі причини, взагалі не повинно бути проблемою.


11

Так, при використанні Reflection є ефективність, але можливе вирішення для оптимізації - кешування методу:

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

це призведе до:

[java] Метод виклику 1000000 разів рефлекторно при пошуку зайняв 5618 мільйонів

[java] Метод виклику 1000000 разів рефлекторно з кешем займав 270 мільйонів


Повторне використання методу / конструктора справді корисне і допомагає, але зауважте, що тест, наведений вище, не дає значущих цифр через звичайні проблеми з бенчмаркінг (немає розминки, тому перший цикл, зокрема, вимірює час нагрівання JVM / JIT).
StaxMan

7

Рефлексія повільна, хоча розподіл об’єктів не настільки безнадійне, як інші аспекти відображення. Досягнення еквівалентної продуктивності за допомогою опису на основі відображення вимагає, щоб ви написали свій код, щоб jit міг визначити, який клас інстанціюється. Якщо ідентифікацію класу неможливо визначити, код виділення не може бути вписаний. Гірше, аналіз втечі не вдається, і об'єкт не може бути виділений стеком. Якщо вам пощастить, профілювання часу запуску JVM може прийти на допомогу, якщо цей код нагріється, і може динамічно визначити, який клас переважає і може оптимізувати цей клас.

Будьте в курсі, що мікробензикові позначки в цій нитці глибоко хибні, тому приймайте їх із зерном солі. Найменш недоліком на сьогодні є Пітер Лоурі: він виконує розминку для отримання методів, і він (свідомо) перемагає аналіз втечі, щоб переконатися, що розподіли дійсно відбуваються. Навіть у цього є свої проблеми: наприклад, можна очікувати, що величезна кількість магазинів масивів переможе кеші та буфери для зберігання, тому це може стати головним орієнтиром пам’яті, якщо ваш розподіл буде дуже швидким. (Кудо Петеру, як правильно зробити висновок: що різниця "150сн", а не "2,5х". Я підозрюю, що він робить подібні речі на життя.)


7

Цікаво, що встановлення setAccessible (true), яке пропускає перевірки безпеки, зменшує вартість на 20%.

Без setAccessible (вірно)

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

За допомогою setAccessible (true)

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

1
Мені це в принципі очевидно. Чи масштабуються ці числа лінійно під час запуску 1000000викликів?
Лукас Едер

Насправді setAccessible()може бути набагато більша різниця в цілому, особливо для методів з декількома аргументами, тому це завжди слід називати.
StaxMan

6

Так, це значно повільніше. Ми виконували якийсь код, який це зробив, і поки у мене немає доступних показників на даний момент, кінцевим результатом було те, що нам довелося переробляти цей код, щоб не використовувати відображення. Якщо ви знаєте, що таке клас, просто зателефонуйте конструктору безпосередньо.


1
+1 У мене був подібний досвід. Добре переконатися, що використовувати рефлексію лише тоді, коли це абсолютно необхідно.
Райан Темза

наприклад, бібліотекам, що базуються на АОП, потрібні роздуми.
gaurav

4

У doReflection () - накладні витрати через Class.forName ("misc.A") (що вимагає пошуку класу, що потенційно сканує шлях класу до фільтрасистеми), а не newInstance (), що викликається в класі. Мені цікаво, як виглядатиме статистика, якщо Class.forName ("misc.A") робиться лише один раз поза межами for-loop, насправді це не потрібно робити для кожного виклику циклу.


1

Так, завжди буде повільніше створювати об’єкт шляхом відображення, оскільки JVM не може оптимізувати код під час компіляції. Детальнішу інформацію див. У навчальних посібниках із відображенням Sun / Java .

Дивіться цей простий тест:

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
Зауважте, що вам слід відокремити пошук ( Class.forName()) від інстанції (newInstance ()), оскільки вони суттєво відрізняються за своїми експлуатаційними характеристиками, і ви можете час від часу уникати повторного пошуку в добре розробленій системі.
Йоахім Зауер

3
Крім того: вам потрібно виконати кожне завдання багато, багато разів, щоб отримати корисний орієнтир: по-перше, дії занадто повільні, щоб надійно виміряти, а по-друге, вам потрібно буде розігріти HotSpot VM, щоб отримати корисні номери.
Йоахім Зауер

1

Часто ви можете використовувати Apache commons BeanUtils або PropertyUtils, які здійснюють самоаналіз (в основному вони кешують метадані про класи, тому їм не завжди потрібно використовувати рефлексію).


0

Думаю, це залежить від того, наскільки легкий / важкий цільовий метод. якщо цільовий метод дуже легкий (наприклад, геттер / сетер), він може бути в 1-3 рази повільніше. якщо цільовий метод займає близько 1 мілісекунди або вище, то продуктивність буде дуже близькою. ось тест, який я зробив з Java 8 та 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;
        }
    }
}

Повний код тесту доступний на GitHub: ReflectionTest.java

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.