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-парсеров. Такое использование часто может выполняться один раз при запуске системы, поэтому небольшая неэффективность в любом случае не имеет значения!
Ответ 6
Возможно, вы найдете эту статью.
Ответ 7
@Tim Bender code дает эти результаты на моей машине (jdk_1.8_45, os_x 10.10, i7, 16G):
Reflecting instantiation took:1139ms
Normal instaniation took: 4969ms
так что, похоже, на современной JVM код отражения также будет оптимизирован.