Большинство программистов знают о reflection, которая (она — рефлексия) упрощает добавление динамических возможностей в статические языки, такие как Java/C#. Однако reflection упрекают в том, что вызовы работают очень медленно — до 500 раз медленнее. Все же это можно c легкостью исправить — покажем в этой статье как сделать reflection-вызов таким же быстрым, как и прямой (direct) вызов.
Замерим скорость выполнения следующего класса:
Я измерял скорость на Core 2 Duo E6300, Windows Vista, JRE 1.6.0_16. Класс Timer — вспомогательный класс для измерения времени. Для измерения будем делать 5 000 000 вызовов каждого из способов. Начнём с прямого доступа к полю класса:
5000000 итераций выполнилось за 44мс. Время может различаться от вызова к вызову, однако в целом результат получается такой. Сделаем тоже самое с использованием reflection:
Выполнилось за 11233мс — более чем 250 раз медленнее! Повторим тоже самое, заменив прямую работу с полями на вызовы методов.
Прямой вызов метода чуть медленнее, чем прямой доступ к полю и выполняется за 51 мс. При этом reflection-доступ к методу чуть быстрее — 4177 ms – около 100 медленнее прямого доступа.
Reflection средствами JVM реализована с использованием медленных JNI-вызовов. CGLIB предоставляет класс FastMethod, предоставляющий ту же функциональность без использования JNI, делая вызовы много, много быстрее:
Результат — 353 мс, что всего в 7 раз медленнее, чем обычный вызов метода. Если бы CGLIB имел аналог FastMethod для полей, мы могли бы сравнить и доступ к полям.
Давайте подумаем, как можно добиться лучшего результата в предыдущем фрагменте. Мы имеем несколько дорогостоящих операций: первая — создание массива в цикле, мы можем обойтись одним массивом для всех итераций цикла. Другая — боксинг (auto-boxing) чисел. Предположим, что число — не число, а обычный объект и не требует боксинга:
Результат — 112мс. Всего лишь в 2 раза медленее, чем обычный вызов метода! Однако, почему же медленее? Дело в том, что FastMethod реализован с использованием шаблона проектирования Decorator. Попробуем декорировать наш класс A и оценим результат:
Выполнилось за 124 мс — получилось даже медленнее, чем FastMethod. И так как каждый хороший программист использует шаблоны проектирования — мы можем сказать, что с некоторой оптимизацией reflection может быть столь же быстрой, как и прямые вызовы без reflection.
Полный листинг:
И результат:
Direct field access: 44
Preparing for reflective field access: 765
Reflective field access: 11233
Direct method access: 51
Preparing for reflective method access: 1020
Reflective method access: 4177
Preparing for fast reflective method access: 1400
Fast reflective method access: 353
Preparing for fast reflective method access (2): 2164
Fast reflective method access (2): 112
Preparing decorator method access: 52
Decorator method access: 124
Можно заметить, что «подготовка» занимает некоторое время, однако эта операция может выполняться всего лишь раз. «Подготовка» стандартных методов reflection занимает меньше 1мс, методов CGLIB — около 100мс. Дело в том, что сгенерированный класс должен быть скомпилирован и загружен class loader'ом. Однако такая генерация может быть выполнения всего лишь раз.
Итак, reflection в Java — не является узким местом при использовании FastMethod из библиотеки CGLIB. И после некоторых оптимизаций вызовы могут быть столь же быстрыми, что и непосредственные вызовы. Теперь можно не бояться использовать эти возможности в своём коде. Однако, важно помнить, что преждевременная оптимизация ведет к проблемам, и необходимо оптимизировать только тот код, который действительно является узким местом вашего приложения.
Примечание переводчика: Я увеличил число итераций в 10 раз и запустил (Intel Q6600, Vista x64, JRE 1.6.0_16 x64) тест привёденный в статье, получил несколько другие результаты — «декорирование» практически не повлияло на время 120 vs 168, а конечный результат оказался в 6 раз медленнее прямого вызова, не в 2 как в статье:
Direct field access: 170
Preparing for reflective field access: 1370
Reflective field access: 33928
Direct method access: 120
Preparing for reflective method access: 3382
Reflective method access: 21208
Preparing for fast reflective method access: 3043
Fast reflective method access: 1247
Preparing for fast reflective method access (2): 13174
Fast reflective method access (2): 741
Preparing decorator method access: 128
Decorator method access: 168
Замерим скорость выполнения следующего класса:
Copy Source | Copy HTML
- class A {
- public int value = 0;
-
- public void add(int x) {
- value += x;
- }
- }
Я измерял скорость на Core 2 Duo E6300, Windows Vista, JRE 1.6.0_16. Класс Timer — вспомогательный класс для измерения времени. Для измерения будем делать 5 000 000 вызовов каждого из способов. Начнём с прямого доступа к полю класса:
Copy Source | Copy HTML
- t.start("Direct field access");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- a.value += i;
- }
- t.stop();
5000000 итераций выполнилось за 44мс. Время может различаться от вызова к вызову, однако в целом результат получается такой. Сделаем тоже самое с использованием reflection:
Copy Source | Copy HTML
- t.start("Preparing for reflective field access");
- Field f = A.class.getField("value");
- t.stop();
-
- t.start("Reflective field access");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- f.set(a, ((Integer) f.get(a)) + i);
- }
- t.stop();
Выполнилось за 11233мс — более чем 250 раз медленнее! Повторим тоже самое, заменив прямую работу с полями на вызовы методов.
Copy Source | Copy HTML
- t.start("Direct method access");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- a.add(i);
- }
- t.stop();
-
- t.start("Preparing for reflective method access");
- Method m = A.class.getDeclaredMethod("add", int.class);
- t.stop();
-
- t.start("Reflective method access");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- m.invoke(a, i);
- }
- t.stop();
Прямой вызов метода чуть медленнее, чем прямой доступ к полю и выполняется за 51 мс. При этом reflection-доступ к методу чуть быстрее — 4177 ms – около 100 медленнее прямого доступа.
Как же это оптимизировать?
Reflection средствами JVM реализована с использованием медленных JNI-вызовов. CGLIB предоставляет класс FastMethod, предоставляющий ту же функциональность без использования JNI, делая вызовы много, много быстрее:
Copy Source | Copy HTML
- t.start("Preparing for fast reflective method access");
- FastClass fc = FastClass.create(A.class);
- FastMethod fm = fc.getMethod(m);
- t.stop();
-
- t.start("Fast reflective method access");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- fm.invoke(a, new Object[]{i});
- }
- t.stop();
Результат — 353 мс, что всего в 7 раз медленнее, чем обычный вызов метода. Если бы CGLIB имел аналог FastMethod для полей, мы могли бы сравнить и доступ к полям.
Давайте подумаем, как можно добиться лучшего результата в предыдущем фрагменте. Мы имеем несколько дорогостоящих операций: первая — создание массива в цикле, мы можем обойтись одним массивом для всех итераций цикла. Другая — боксинг (auto-boxing) чисел. Предположим, что число — не число, а обычный объект и не требует боксинга:
Copy Source | Copy HTML
- class Ref {
- int value;
- }
-
- class A {
- public int value = 0;
-
- public void add(Ref ref) {
- value += ref.value;
- }
- }
-
- ...
-
- t.start("Preparing for fast reflective method access (2)");
- FastClass fc2 = FastClass.create(A.class);
- FastMethod fm2 = fc2.getMethod("add", new Class[]{Ref.class});
- Ref ref = new Ref();
- Object[] arguments = new Object[]{ref};
- t.stop();
-
- t.start("Fast reflective method access (2)");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- ref.value = i;
- fm2.invoke(a, arguments);
- }
- t.stop();
Результат — 112мс. Всего лишь в 2 раза медленее, чем обычный вызов метода! Однако, почему же медленее? Дело в том, что FastMethod реализован с использованием шаблона проектирования Decorator. Попробуем декорировать наш класс A и оценим результат:
Copy Source | Copy HTML
- interface AIf {
- public void add(int x);
- }
-
- class A implements AIf {
- public int value = 0;
-
- public void add(int x) {
- value += x;
- }
- }
-
- class ADecorator implements AIf {
- private AIf a;
-
- public ADecorator(AIf a) {
- this.a = a;
- }
-
- public void add(int x) {
- a.add(x);
- }
- }
- ...
- t.start("Preparing decorator method access");
- AIf d = new ADecorator(a);
- t.stop();
-
- t.start("Decorator method access");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- d.add(i);
- }
- t.stop();
Выполнилось за 124 мс — получилось даже медленнее, чем FastMethod. И так как каждый хороший программист использует шаблоны проектирования — мы можем сказать, что с некоторой оптимизацией reflection может быть столь же быстрой, как и прямые вызовы без reflection.
Полный листинг:
Copy Source | Copy HTML
- package reflection;
-
- import net.sf.cglib.reflect.FastClass;
- import net.sf.cglib.reflect.FastMethod;
-
- import java.lang.reflect.Field;
- import java.lang.reflect.InvocationTargetException;
- import java.lang.reflect.Method;
- import java.util.LinkedHashMap;
- import java.util.Map;
-
- class Timer {
- private long startTime = 0;
-
- private String msg = null;
-
- private Map<String, Long> map = new LinkedHashMap<String, Long>();
-
- public void start(String msg) {
- if (startTime != 0) {
- throw new IllegalStateException("Already started");
- }
- startTime = System.currentTimeMillis();
- this.msg = msg;
- }
-
- public void stop() {
- if (startTime == 0) {
- throw new IllegalStateException("Not started");
- }
- long now = System.currentTimeMillis();
- Long n = map.get(msg);
- if (n == null) {
- n = 0l;
- }
- n += (now - startTime);
- map.put(msg, n);
- startTime = 0;
- msg = null;
- }
-
- public void output() {
- for (String msg : map.keySet()) {
- System.out.println(msg + ": " + map.get(msg));
- }
- }
- }
-
- class Ref {
- int value;
- }
-
- interface AIf {
- public void add(int x);
- }
-
- class A implements AIf {
- public int value = 0;
-
- public void add(int x) {
- value += x;
- }
-
- public void add(Ref ref) {
- value += ref.value;
- }
- }
-
- class ADecorator implements AIf {
- private AIf a;
-
- public ADecorator(AIf a) {
- this.a = a;
- }
-
- public void add(int x) {
- a.add(x);
- }
- }
-
- public class Reflect {
- private static final int TOTAL_LOOP_COUNT = 5000000;
-
- /**<br/> * How many loops to do in one step.<br/> */
- private static final int LOOPS_IN_STEP_COUNT = 100;
-
- /**<br/> * How many steps to do - in each step there will be<br/> * {@link #LOOPS_IN_STEP_COUNT} loops.<br/> */
- private static final int STEP_COUNT = TOTAL_LOOP_COUNT / LOOPS_IN_STEP_COUNT;
-
- public static void main(String[] args) throws SecurityException,
- NoSuchFieldException, IllegalArgumentException,
- IllegalAccessException, NoSuchMethodException,
- InvocationTargetException {
-
- Timer t = new Timer();
- A a = new A();
-
- for (int j = 0; j < STEP_COUNT; ++j) {
-
- t.start("Direct field access");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- a.value += i;
- }
- t.stop();
-
- t.start("Preparing for reflective field access");
- Field f = A.class.getField("value");
- t.stop();
-
- t.start("Reflective field access");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- f.set(a, ((Integer) f.get(a)) + i);
- }
- t.stop();
-
- t.start("Direct method access");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- a.add(i);
- }
- t.stop();
-
- t.start("Preparing for reflective method access");
- Method m = A.class.getDeclaredMethod("add", int.class);
- t.stop();
-
- t.start("Reflective method access");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- m.invoke(a, i);
- }
- t.stop();
-
- t.start("Preparing for fast reflective method access");
- FastClass fc = FastClass.create(A.class);
- FastMethod fm = fc.getMethod(m);
- t.stop();
-
- t.start("Fast reflective method access");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- fm.invoke(a, new Object[]{i});
- }
- t.stop();
-
- t.start("Preparing for fast reflective method access (2)");
- FastClass fc2 = FastClass.create(A.class);
- FastMethod fm2 = fc2.getMethod("add", new Class[]{
- Ref.class
- });
- Ref ref = new Ref();
- Object[] arguments = new Object[]{ref};
- t.stop();
-
- t.start("Fast reflective method access (2)");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- ref.value = i;
- fm2.invoke(a, arguments);
- }
- t.stop();
-
- t.start("Preparing decorator method access");
- AIf d = new ADecorator(a);
- t.stop();
-
- t.start("Decorator method access");
- for (int i = 0; i < LOOPS_IN_STEP_COUNT; ++i) {
- d.add(i);
- }
- t.stop();
-
- }
-
- t.output();
- }
- }
-
И результат:
Direct field access: 44
Preparing for reflective field access: 765
Reflective field access: 11233
Direct method access: 51
Preparing for reflective method access: 1020
Reflective method access: 4177
Preparing for fast reflective method access: 1400
Fast reflective method access: 353
Preparing for fast reflective method access (2): 2164
Fast reflective method access (2): 112
Preparing decorator method access: 52
Decorator method access: 124
Можно заметить, что «подготовка» занимает некоторое время, однако эта операция может выполняться всего лишь раз. «Подготовка» стандартных методов reflection занимает меньше 1мс, методов CGLIB — около 100мс. Дело в том, что сгенерированный класс должен быть скомпилирован и загружен class loader'ом. Однако такая генерация может быть выполнения всего лишь раз.
Итак, reflection в Java — не является узким местом при использовании FastMethod из библиотеки CGLIB. И после некоторых оптимизаций вызовы могут быть столь же быстрыми, что и непосредственные вызовы. Теперь можно не бояться использовать эти возможности в своём коде. Однако, важно помнить, что преждевременная оптимизация ведет к проблемам, и необходимо оптимизировать только тот код, который действительно является узким местом вашего приложения.
Примечание переводчика: Я увеличил число итераций в 10 раз и запустил (Intel Q6600, Vista x64, JRE 1.6.0_16 x64) тест привёденный в статье, получил несколько другие результаты — «декорирование» практически не повлияло на время 120 vs 168, а конечный результат оказался в 6 раз медленнее прямого вызова, не в 2 как в статье:
Direct field access: 170
Preparing for reflective field access: 1370
Reflective field access: 33928
Direct method access: 120
Preparing for reflective method access: 3382
Reflective method access: 21208
Preparing for fast reflective method access: 3043
Fast reflective method access: 1247
Preparing for fast reflective method access (2): 13174
Fast reflective method access (2): 741
Preparing decorator method access: 128
Decorator method access: 168