Java Reflection: почему это так медленно?

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

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

class B {

}

public class Test {

    public static long timeDiff(long old) {
        return System.currentTimeMillis() - old;
    }

    public static void main(String args[]) throws Exception {

        long numTrials = (long) Math.pow(10, 7);

        long millis;

        millis = System.currentTimeMillis();

        for (int i=0; i<numTrials; i++) {
            new B();
        }
        System.out.println("Normal instaniation took: "
                 + timeDiff(millis) + "ms");

        millis = System.currentTimeMillis();

        Class<B> c = B.class;

        for (int i=0; i<numTrials; i++) {
            c.newInstance();
        }

        System.out.println("Reflecting instantiation took:" 
              + timeDiff(millis) + "ms");

    }
}

Итак, мои вопросы:

  • Почему это так медленно? Есть ли что-то, что я делаю неправильно? (даже пример выше показывает разницу). Я с трудом верю, что он может быть на 100% медленнее, чем обычно.

  • Есть ли что-то еще, что может быть лучше использовано для обработки кода как данных (помните, что я застрял с Java на данный момент)

Ответ 1

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

Для тех, кто задавался вопросом, что такое время, я добавил фазу разминки и использовал массив для поддержки созданных объектов (более похожих на то, что может сделать реальная программа). Я запустил тестовый код на моей OSX системе jdk7 и получил следующее:

Отражение создания экземпляра: 5180ms
Нормальное вмешательство: 2001мс

Модифицированный тест:

public class Test {

    static class B {

    }

    public static long timeDiff(long old) {
        return System.nanoTime() - old;
    }

    public static void main(String args[]) throws Exception {

        int numTrials = 10000000;
        B[] bees = new B[numTrials];
        Class<B> c = B.class;
        for (int i = 0; i < numTrials; i++) {
            bees[i] = c.newInstance();
        }
        for (int i = 0; i < numTrials; i++) {
            bees[i] = new B();
        }

        long nanos;

        nanos = System.nanoTime();
        for (int i = 0; i < numTrials; i++) {
            bees[i] = c.newInstance();
        }
        System.out.println("Reflecting instantiation took:" + TimeUnit.NANOSECONDS.toMillis(timeDiff(nanos)) + "ms");

        nanos = System.nanoTime();
        for (int i = 0; i < numTrials; i++) {
            bees[i] = new B();
        }
        System.out.println("Normal instaniation took: " + TimeUnit.NANOSECONDS.toMillis(timeDiff(nanos)) + "ms");
    }


}

Ответ 2

Отражение происходит медленно по нескольким очевидным причинам:

  • Компилятор не может делать никакой оптимизации, поскольку не может иметь никакого реального представления о том, что вы делаете. Вероятно, это относится и к JIT, а также к
  • Все, что вы вызываете/создаете, должно быть обнаружено (т.е. классы, просмотренные по имени, методы, которые просматриваются для совпадений и т.д.).
  • Аргументы должны быть одеты с помощью бокса/распаковки, упаковки в массивы, Exceptions завернуты в InvocationTargetException и повторно выбраны и т.д.
  • Вся обработка, которую упоминает здесь Jon Skeet.

Просто потому, что что-то на 100 раз медленнее, это не значит, что вы слишком медленны, считая, что отражение - это "правильный путь" для разработки вашей программы. Например, я полагаю, что среда IDE сильно использует отражение, и моя среда IDE в основном в порядке с точки зрения производительности.

В конце концов, служебная информация отражения, скорее всего, будет бледной в незначительности, когда по сравнению с, скажем, разбора XML или доступа база данных!

Еще один момент, который следует помнить, заключается в том, что микро-тесты являются заведомо ошибочным механизмом для определения того, насколько быстро что-то на практике. Как и замечания Тима Бендера, JVM требует времени для "разогрева", JIT может повторно оптимизировать горячие точки кода на лету и т.д.

Ответ 3

Код JIT для создания экземпляра B невероятно легкий. В принципе, ему необходимо выделить достаточно памяти (которая просто увеличивает указатель, если не требуется GC), и что об этом - нет кода конструктора, который можно действительно называть; Я не знаю, пропускает ли JIT или нет, но в любом случае это не так много.

Сравните это со всем, что должно делать отражение:

  • Убедитесь, что конструктор без параметров
  • Проверьте доступность конструктора без параметров
  • Убедитесь, что у вызывающего есть доступ к использованию отражения вообще
  • Разработайте (во время выполнения), сколько места должно быть выделено
  • Вызов кода конструктора (потому что он не будет знать заранее, что конструктор пуст)

... и, возможно, другие вещи, о которых я даже не думал.

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

Ответ 4

Кажется, что если вы сделаете конструктор доступным, он будет выполняться намного быстрее. Теперь он всего в 10-20 раз медленнее, чем другая версия.

    Constructor<B> c = B.class.getDeclaredConstructor();
    c.setAccessible(true);
    for (int i = 0; i < numTrials; i++) {
        c.newInstance();
    }

Normal instaniation took: 47ms
Reflecting instantiation took:718ms

А если вы используете виртуальную машину сервера, она сможет оптимизировать ее больше, так что она будет работать только в 3-4 раза медленнее. Это довольно типичное представление. Статья, на которую ссылается Гео, хороша для чтения.

Normal instaniation took: 47ms
Reflecting instantiation took:140ms

Но если вы включите скалярную замену с помощью -XX: + DoEscapeAnalysis, то JVM сможет оптимизировать обычное создание экземпляров (это будет 0-15 мс), но рефлексивное создание останется прежним.

Ответ 5

  • Отражение было очень медленным при первом введении, но значительно ускорилось в новых JRE
  • Тем не менее, неплохо было бы использовать отражение во внутреннем цикле
  • Код на основе отражения имеет низкий потенциал для оптимизации на основе JIT
  • Отражение в основном используется при подключении слабосвязанных компонентов, то есть в поиске конкретных классов и методов, где известны только интерфейсы: взаимозависимые фреймворки, создание экземпляров реализации JDBC или XML-парсеров. Такое использование часто может выполняться один раз при запуске системы, поэтому небольшая неэффективность в любом случае не имеет значения!

Ответ 7

@Tim Bender code дает эти результаты на моей машине (jdk_1.8_45, os_x 10.10, i7, 16G):

Reflecting instantiation took:1139ms Normal instaniation took: 4969ms

так что, похоже, на современной JVM код отражения также будет оптимизирован.