Производительность отражения Java

172

Приводит ли создание объекта с использованием отражения вместо вызова конструктора класса какие-либо существенные различия в производительности?

dmanxiii
источник

Ответы:

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 или около того. В зависимости от версии виртуальной машины, разница может быть ближе, чем вы думаете с соответствующей стратегией кэширования.
Шон Рейли
56
@Peter Lawrey ниже указал, что этот тест был полностью недействительным, потому что компилятор оптимизировал неотражающее решение (он может даже доказать, что ничего не сделано, и оптимизировать цикл for). Необходимо переработать и, вероятно, удалить из SO как неверную / вводящую в заблуждение информацию. Кэшируйте созданные объекты в массиве в обоих случаях, чтобы оптимизатор не смог их оптимизировать. (Он не может сделать это в отражающей ситуации, потому что не может доказать, что конструктор не имеет побочных эффектов)
Билл К,
6
@ Билл К - давайте не будем увлекаться. Да, номера отключены из-за оптимизации. Нет, тест не является полностью недействительным. Я добавил вызов, который устраняет любую возможность искажения результата, и числа все еще сложены против отражения. В любом случае, помните, что это очень грубый микропроцессор, который просто показывает, что отражение всегда влечет за собой определенные накладные расходы
Ювал Адам
4
Это, вероятно, бесполезный тест. В зависимости от того, что делает что-то. Если он ничего не делает с видимым побочным эффектом, тогда ваш тест запускает только мертвый код.
nes1983
9
Я только что был свидетелем оптимизации JVM в 35 раз. Повторный запуск теста в цикле - это то, как вы тестируете оптимизированный код. Первая итерация: 3045 мс, вторая итерация: 2941 мс, третья итерация: 90 мс, четвертая итерация: 83 мс. Код: c.newInstance (я). с конструктор. Неотражающий код: новый A (i), который дает 13, 4, 3 .. мс раз. Так что да, рефлексия в этом случае была медленной, но не настолько медленной, как те, что делают люди, потому что при каждом тесте, который я вижу, они просто запускают тест один раз, не давая JVM возможности заменить байтовые коды на машину код.
Майк
87

Да, это медленнее.

Но помните чертово правило № 1 - ОПТИМИЗАЦИЯ ПРЕДВАРИТЕЛЬНОЙ СИЛЫ - КОРЕНЬ ВСЕГО ЗЛА

(Ну, может быть связано с № 1 для СУХОГО)

Клянусь, если бы кто-нибудь подошел ко мне на работе и спросил меня об этом, я бы очень внимательно следил за их кодом в течение следующих нескольких месяцев.

Вы никогда не должны оптимизировать, пока не будете уверены, что вам это нужно, до тех пор просто пишите хороший, читаемый код.

О, и я не имею в виду писать глупый код тоже. Просто подумайте о том, как сделать это самым чистым способом - без копирования и вставки и т.д. Плохое программирование)

Меня пугает, когда я слышу подобные вопросы, но потом я забываю, что все должны пройти изучение всех правил самостоятельно, прежде чем они действительно получат это. Вы получите это после того, как потратили человек-месяц на отладку чего-то, что кто-то «оптимизировал».

РЕДАКТИРОВАТЬ:

Интересная вещь произошла в этой теме. Посмотрите ответ № 1, это пример того, насколько мощным компилятор является в оптимизации вещей. Тест полностью недействителен, потому что неотражающий экземпляр может быть полностью исключен.

Урок? Никогда не оптимизируйте, пока вы не написали чистое, аккуратно закодированное решение и не доказали, что оно слишком медленное.

Билл К
источник
28
Я полностью согласен с мнением об этом ответе, однако, если вы собираетесь принять важное дизайнерское решение, это помогает иметь представление о производительности, чтобы вы не пошли по совершенно неосуществимому пути. Может, он просто проводит тщательную проверку?
Лимбическая система
26
-1: Избегать неправильных действий - это не оптимизация, это просто делать вещи. Оптимизация делает вещи неправильно, сложным образом из-за реальных или мнимых проблем производительности.
сору
5
@ Соро полностью согласен. Выбор связанного списка над списком массивов для сортировки вставкой - просто верный способ сделать что-то. Но этот конкретный вопрос - есть хорошие варианты использования для обеих сторон исходного вопроса, поэтому было бы неправильно выбирать один из них, основанный на производительности, а не на самом подходящем решении. Я не уверен, что мы вообще не согласны, поэтому я не уверен, почему вы сказали «-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, разница составляет 95 нс против 100 нс. Вместо того, чтобы хранить A в массиве, я храню hashCodes. Если вы говорите -verbose: class, вы можете видеть, когда горячая точка генерирует байт-код для построения A и сопровождающего его ускорения.
Рон
@PeterLawrey Если я посмотрю один раз (один вызов Class.getDeclaredMethod), а затем позвоню Method.invokeнесколько раз? Я использую отражение один раз или столько раз, сколько я его вызываю? Последующий вопрос, а что если вместо Methodнего это Constructorсделать Constructor.newInstanceнесколько раз?
tmj
28

Если действительно нужно что-то быстрее, чем отражение, и это не просто преждевременная оптимизация, то генерация байт-кода с помощью ASM вариант или библиотеки более высокого уровня - вариант. Генерация байт-кода в первый раз медленнее, чем просто использование отражения, но как только байт-код сгенерирован, он работает так же быстро, как обычный код Java, и будет оптимизирован компилятором JIT.

Некоторые примеры приложений, которые используют генерацию кода:

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

  • JSerial генерирует байт-код для чтения / записи полей сериализованных объектов вместо использования отражения. На сайте JSerial есть несколько тестов .

  • Я не уверен на 100% (и сейчас мне не хочется читать исходники), но я думаю, что Guice генерирует байт-код для внедрения зависимостей. Поправьте меня если я ошибаюсь.

Эско Луонтола
источник
27

«Значительный» полностью зависит от контекста.

Если вы используете рефлексию для создания одного объекта-обработчика на основе какого-либо файла конфигурации, а затем тратите оставшееся время на выполнение запросов к базе данных, то это не имеет значения. Если вы создаете большое количество объектов с помощью отражения в узком цикле, то да, это важно.

В общем, гибкость дизайна (где это необходимо!) Должна стимулировать использование отражения, а не производительности. Однако, чтобы определить, является ли производительность проблемой, вам нужно профилировать, а не получать произвольные ответы от дискуссионного форума.

kdgregory
источник
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 миллис

mel3kings
источник
Повторное использование метода / конструктора действительно полезно и помогает, но обратите внимание, что приведенный выше тест не дает значимых чисел из-за обычных проблем с эталонным тестом (без прогрева, поэтому, в частности, первый цикл в основном измеряет время прогрева JVM / JIT).
StaxMan
7

Отражение медленное, хотя распределение объектов не так безнадежно, как другие аспекты отражения. Достижение эквивалентной производительности с помощью основанной на отражении реализации требует от вас написания кода, чтобы jit мог определить, какой класс создается. Если идентичность класса не может быть определена, тогда код распределения не может быть встроен. Хуже того, escape-анализ завершается неудачно, и объект не может быть размещен в стеке. Если вам повезет, профилирование во время выполнения JVM может прийти на помощь, если этот код перегреется, и может динамически определить, какой класс преобладает, и может оптимизировать его.

Помните, что микробенчмарки в этой теме глубоко испорчены, поэтому возьмите их с небольшим количеством соли. Наименьший недостаток на сегодняшний день - у Питера Лоури: он выполняет прогрев прогона, чтобы соединить методы, и он (сознательно) побеждает анализ побега, чтобы убедиться, что распределение действительно происходит. Но даже у этого есть свои проблемы: например, можно ожидать, что огромное количество хранилищ массивов побеждает кеши и буферы хранилищ, так что в конечном итоге это будет в основном ориентиром для памяти, если ваши выделения очень быстрые. (Благодарность Питеру за то, что он сделал правильное заключение: разница составляет «150 нс», а не «2,5х». Я подозреваю, что он зарабатывает этим на жизнь.)

Doradus
источник
7

Интересно, что установка setAccessible (true), которая пропускает проверки безопасности, снижает стоимость на 20%.

Без setAccessible (true)

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

Да, это значительно медленнее. Мы выполняли некоторый код, который сделал это, и хотя у меня пока нет доступных метрик, конечным результатом было то, что нам пришлось реорганизовать этот код, чтобы не использовать отражение. Если вы знаете, что это за класс, просто вызовите конструктор напрямую.

Elie
источник
1
+1 У меня был похожий опыт. Хорошо использовать рефлексию, только если это абсолютно необходимо.
Райан Темс
Например, библиотеки на основе АОП нуждаются в рефлексии.
Гаурав
4

В doReflection () это накладные расходы из-за Class.forName («misc.A») (что потребует поиска класса, потенциально сканирующего путь к классу в файловой системе), а не newInstance (), вызываемого в классе. Мне интересно, как будет выглядеть статистика, если Class.forName ("misc.A") выполняется только один раз вне цикла for, это не обязательно должно выполняться для каждого вызова цикла.

tikoo
источник
1

Да, всегда будет медленнее создавать объект путем отражения, потому что JVM не может оптимизировать код во время компиляции. См. Учебники Sun / Java Reflection для получения более подробной информации.

Смотрите этот простой тест:

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
источник
3
Обратите внимание, что вы должны отделить lookup ( Class.forName()) от экземпляра (newInstance ()), потому что они значительно различаются по своим характеристикам производительности, и вы можете иногда избегать повторного поиска в хорошо спроектированной системе.
Иоахим Зауэр
3
Кроме того: вам нужно выполнить каждую задачу много-много раз, чтобы получить полезный тест: во-первых, действия слишком медленные, чтобы их можно было надежно измерить, а во-вторых, вам нужно прогреть виртуальную машину HotSpot, чтобы получить полезные значения.
Иоахим Зауэр
1

Часто вы можете использовать Apache Commons BeanUtils или PropertyUtils, которые самоанализ (в основном они кэшируют метаданные о классах, поэтому им не всегда нужно использовать отражение).

sproketboy
источник
0

Я думаю, это зависит от того, насколько легким / тяжелым является целевой метод. если целевой метод очень легкий (например, метод получения / установки), он может быть в 1-3 раза медленнее. если целевой метод занимает около 1 миллисекунды или выше, производительность будет очень близка. Вот тест, который я сделал с Java 8 и refleasm :

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

user_3380739
источник