Проблема производительности отражения Java - PullRequest
7 голосов
/ 04 августа 2011

Я знаю, что есть много тем, говорящих о производительности Reflection.

Даже официальные документы Java говорят, что Reflection медленнее, но у меня есть этот код:

  public class ReflectionTest {
   public static void main(String[] args) throws Exception {
       Object object = new Object();
       Class<Object> c = Object.class;

       int loops = 100000;

       long start = System.currentTimeMillis();
       Object s;
       for (int i = 0; i < loops; i++) {
           s = object.toString();
           System.out.println(s);
       }
       long regularCalls = System.currentTimeMillis() - start;
       java.lang.reflect.Method method = c.getMethod("toString");

       start = System.currentTimeMillis();
       for (int i = 0; i < loops; i++) {
           s = method.invoke(object);
           System.out.println(s);
       }

       long reflectiveCalls = System.currentTimeMillis() - start;

       start = System.currentTimeMillis();
       for (int i = 0; i < loops; i++) {
           method = c.getMethod("toString");
           s = method.invoke(object);
           System.out.println(s);
       }

       long reflectiveLookup = System.currentTimeMillis() - start;

       System.out.println(loops + " regular method calls:" + regularCalls
               + " milliseconds.");

       System.out.println(loops + " reflective method calls without lookup:"
               + reflectiveCalls+ " milliseconds.");

       System.out.println(loops + " reflective method calls with lookup:"
               + reflectiveLookup + " milliseconds.");

   }

}

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

Но это печатает это:

100000 regular method calls:1129 milliseconds.
100000 reflective method calls without lookup:910 milliseconds.
100000 reflective method calls with lookup:994 milliseconds.

Просто для заметки: сначала я выполнил его без этой кучки системных вызовов, а затем понял, что некоторая оптимизация JVM просто делает это быстрее, поэтому я добавил эти printls, чтобы посмотреть, было ли отражение еще быстрее.

Результат без sysout:

100000 regular method calls:68 milliseconds.
100000 reflective method calls without lookup:48 milliseconds.
100000 reflective method calls with lookup:168 milliseconds.

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

Следуя инструкциям, я запустил каждый цикл отдельно, и результат (без sysouts)

100000 regular method calls:70 milliseconds.
100000 reflective method calls without lookup:120 milliseconds.
100000 reflective method calls with lookup:129 milliseconds.

Ответы [ 8 ]

12 голосов
/ 05 августа 2011

Никогда не тестируйте производительность разных битов кода в одном «прогоне». JVM имеет различные оптимизации, которые означают, что, хотя конечный результат один и тот же, то, как выполняются внутренние функции, может отличаться. Говоря более конкретно, во время вашего теста JVM, возможно, заметила, что вы часто вызываете Object.toString, и начала вставлять вызовы методов в Object.toString. Возможно, он начал выполнять развертывание цикла. Или в первом цикле могла быть сборка мусора, но не во втором или третьем циклах.

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

Результаты на моем компьютере (без печати и 1 000 000 прогонов каждый)

Все три цикла выполняются в одной программе

1000000 обычных вызовов метода: 490 миллисекунд.

1000000 вызовов метода отражения без поиска: 393 миллисекунды.

1000000 вызовов метода отражения с циклом: 978 миллисекунд.

Циклы запускаются в отдельных программах

1000000 обычных вызовов метода: 475 миллисекунд.

1000000 вызовов метода отражения без поиска: 555 миллисекунд.

1000000 вызовов метода отражения с циклом: 1160 миллисекунд.

5 голосов
/ 05 августа 2011

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

3 голосов
/ 05 августа 2011

Нет внутренней причины, по которой отражающий вызов должен быть медленнее, чем обычный вызов. JVM может оптимизировать их в одно и то же.

Практически человеческие ресурсы ограничены, и им пришлось сначала оптимизировать обычные звонки. Со временем они могут работать над оптимизацией отражающих вызовов; особенно когда отражение становится все более и более популярным.

3 голосов
/ 05 августа 2011

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

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

Результаты, которые я получаю

Average regular method calls:2 ns.
Average reflective method calls without lookup:10 ns.
Average reflective method calls with lookup:240 ns.

код

import java.lang.reflect.Method;

public class ReflectionTest {
    public static void main(String[] args) throws Exception {
        int loops = 1000 * 1000;

        Object object = new Object();
        long start = System.nanoTime();
        Object s;
        testMethodCall(object, loops);
        long regularCalls = System.nanoTime() - start;
        java.lang.reflect.Method method = Object.class.getMethod("getClass");
        method.setAccessible(true);

        start = System.nanoTime();
        testInvoke(object, loops, method);

        long reflectiveCalls = System.nanoTime() - start;

        start = System.nanoTime();
        testGetMethodInvoke(object, loops);

        long reflectiveLookup = System.nanoTime() - start;

        System.out.println("Average regular method calls:"
                + regularCalls / loops + " ns.");

        System.out.println("Average reflective method calls without lookup:"
                + reflectiveCalls / loops + " ns.");

        System.out.println("Average reflective method calls with lookup:"
                + reflectiveLookup / loops + " ns.");

    }

    private static Object testMethodCall(Object object, int loops) {
        Object s = null;
        for (int i = 0; i < loops; i++) {
            s = object.getClass();
        }
        return s;
    }

    private static Object testInvoke(Object object, int loops, Method method) throws Exception {
        Object s = null;
        for (int i = 0; i < loops; i++) {
            s = method.invoke(object);
        }
        return s;
    }

    private static Object testGetMethodInvoke(Object object, int loops) throws Exception {
        Method method;
        Object s = null;
        for (int i = 0; i < loops; i++) {
            method = Object.class.getMethod("getClass");
            s = method.invoke(object);
        }
        return s;
    }
}
3 голосов
/ 05 августа 2011

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

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

2 голосов
/ 05 августа 2011

Я писал свой собственный микро-тест, без циклов и с System.nanoTime():

public static void main(String[] args) throws NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException
{
  Object obj = new Object();
  Class<Object> objClass = Object.class;
  String s;

  long start = System.nanoTime();
  s = obj.toString();
  long directInvokeEnd = System.nanoTime();
  System.out.println(s);
  long methodLookupStart = System.nanoTime();
  java.lang.reflect.Method method = objClass.getMethod("toString");
  long methodLookupEnd = System.nanoTime();
  s = (String) (method.invoke(obj));
  long reflectInvokeEnd = System.nanoTime();
  System.out.println(s);
  System.out.println(directInvokeEnd - start);
  System.out.println(methodLookupEnd - methodLookupStart);
  System.out.println(reflectInvokeEnd - methodLookupEnd);
}

Я выполнял это в Eclipse на моей машине дюжину раз, и результаты меняются весьманемного, но вот что я обычно получаю:

  • часы прямого вызова метода в 40-50 микросекунд
  • часы поиска метода в 150-200 микросекундах
  • Рефлексивный вызов с переменным тактовым сигналом 250-310 мксдокументация, если они говорят, что отражение намного медленнее, чем прямой вызов.
1 голос
/ 15 января 2015

Даже если вы посмотрите на метод в обоих случаях (т.е. до 2-го и 3-го цикла), первый поиск занимает намного меньше времени, чем второй поиск, который должен был быть наоборот, и меньше, чем обычный вызов метода на моей машине.

Тем не менее, если вы используете 2-й цикл с поиском метода и оператор System.out.println, я получу это:

regular call        : 740 ms
look up(2nd loop)   : 640 ms
look up ( 3rd loop) : 800 ms

Без System.out.println заявления я получаю:

regular call    : 78 ms
look up (2nd)   : 37 ms
look up (3rd )  : 112 ms
1 голос
/ 08 сентября 2013

Меня удивляет, что вы поместили вызов System.out.println (s) внутри своего внутреннего цикла тестирования.Поскольку выполнение ввода-вывода обязательно должно быть медленным, оно фактически «проглатывает» ваш тест, и издержки вызова становятся незначительными.

Попробуйте удалить вызов «println ()» и запустить такой код, яуверен, что вы будете удивлены результатом (некоторые глупые вычисления необходимы, чтобы компилятор вообще не оптимизировал вызовы):

public class Experius
{

    public static void main(String[] args) throws Exception
    {
        Experius a = new Experius();
        int count = 10000000;
        int v = 0;

        long tm = System.currentTimeMillis();
        for ( int i = 0; i < count; ++i )
        {
            v = a.something(i + v);
            ++v;
        }
        tm = System.currentTimeMillis() - tm;

        System.out.println("Time: " + tm);


        tm = System.currentTimeMillis();
        Method method = Experius.class.getMethod("something", Integer.TYPE);
        for ( int i = 0; i < count; ++i )
        {
            Object o = method.invoke(a, i + v);
            ++v;
        }
        tm = System.currentTimeMillis() - tm;

        System.out.println("Time: " + tm);
    }

    public int something(int n)
    {
        return n + 5;
    }

}

- TR

...