Делаем reflection быстрой как прямые вызовы перевод

JAVA*
Большинство программистов знают о reflection, которая (она — рефлексия) упрощает добавление динамических возможностей в статические языки, такие как Java/C#. Однако reflection упрекают в том, что вызовы работают очень медленно — до 500 раз медленнее. Все же это можно c легкостью исправить — покажем в этой статье как сделать reflection-вызов таким же быстрым, как и прямой (direct) вызов.


Замерим скорость выполнения следующего класса:

Copy Source | Copy HTML
  1. class A {
  2.     public int value =  0;
  3.  
  4.     public void add(int x) {
  5.         value += x;
  6.     }
  7. }


Я измерял скорость на Core 2 Duo E6300, Windows Vista, JRE 1.6.0_16. Класс Timer — вспомогательный класс для измерения времени. Для измерения будем делать 5 000 000 вызовов каждого из способов. Начнём с прямого доступа к полю класса:

Copy Source | Copy HTML
  1. t.start("Direct field access");
  2. for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  3.     a.value += i;
  4. }
  5. t.stop();


5000000 итераций выполнилось за 44мс. Время может различаться от вызова к вызову, однако в целом результат получается такой. Сделаем тоже самое с использованием reflection:

Copy Source | Copy HTML
  1. t.start("Preparing for reflective field access");
  2. Field f = A.class.getField("value");
  3. t.stop();
  4.  
  5. t.start("Reflective field access");
  6. for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  7.     f.set(a, ((Integer) f.get(a)) + i);
  8. }
  9. t.stop();


Выполнилось за 11233мс — более чем 250 раз медленнее! Повторим тоже самое, заменив прямую работу с полями на вызовы методов.

Copy Source | Copy HTML
  1. t.start("Direct method access");
  2. for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  3.     a.add(i);
  4. }
  5. t.stop();
  6.  
  7. t.start("Preparing for reflective method access");
  8. Method m = A.class.getDeclaredMethod("add", int.class);
  9. t.stop();
  10.  
  11. t.start("Reflective method access");
  12. for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  13.     m.invoke(a, i);
  14. }
  15. t.stop();


Прямой вызов метода чуть медленнее, чем прямой доступ к полю и выполняется за 51 мс. При этом reflection-доступ к методу чуть быстрее — 4177 ms – около 100 медленнее прямого доступа.

Как же это оптимизировать?


Reflection средствами JVM реализована с использованием медленных JNI-вызовов. CGLIB предоставляет класс FastMethod, предоставляющий ту же функциональность без использования JNI, делая вызовы много, много быстрее:

Copy Source | Copy HTML
  1. t.start("Preparing for fast reflective method access");
  2. FastClass fc = FastClass.create(A.class);
  3. FastMethod fm = fc.getMethod(m);
  4. t.stop();
  5.  
  6. t.start("Fast reflective method access");
  7. for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  8.     fm.invoke(a, new Object[]{i});
  9. }
  10. t.stop();


Результат — 353 мс, что всего в 7 раз медленнее, чем обычный вызов метода. Если бы CGLIB имел аналог FastMethod для полей, мы могли бы сравнить и доступ к полям.

Давайте подумаем, как можно добиться лучшего результата в предыдущем фрагменте. Мы имеем несколько дорогостоящих операций: первая — создание массива в цикле, мы можем обойтись одним массивом для всех итераций цикла. Другая — боксинг (auto-boxing) чисел. Предположим, что число — не число, а обычный объект и не требует боксинга:

Copy Source | Copy HTML
  1. class Ref {
  2.     int value;
  3. }
  4.  
  5. class A {
  6.     public int value =  0;
  7.  
  8.     public void add(Ref ref) {
  9.         value += ref.value;
  10.     }
  11. }
  12.  
  13. ...
  14.  
  15. t.start("Preparing for fast reflective method access (2)");
  16. FastClass fc2 = FastClass.create(A.class);
  17. FastMethod fm2 = fc2.getMethod("add", new Class[]{Ref.class});
  18. Ref ref = new Ref();
  19. Object[] arguments = new Object[]{ref};
  20. t.stop();
  21.  
  22. t.start("Fast reflective method access (2)");
  23. for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  24.     ref.value = i;
  25.     fm2.invoke(a, arguments);
  26. }
  27. t.stop();


Результат — 112мс. Всего лишь в 2 раза медленее, чем обычный вызов метода! Однако, почему же медленее? Дело в том, что FastMethod реализован с использованием шаблона проектирования Decorator. Попробуем декорировать наш класс A и оценим результат:

Copy Source | Copy HTML
  1. interface AIf {
  2.     public void add(int x);
  3. }
  4.  
  5. class A implements AIf {
  6.     public int value =  0;
  7.  
  8.     public void add(int x) {
  9.         value += x;
  10.     }
  11. }
  12.  
  13. class ADecorator implements AIf {
  14.     private AIf a;
  15.  
  16.     public ADecorator(AIf a) {
  17.         this.a = a;
  18.     }
  19.  
  20.     public void add(int x) {
  21.         a.add(x);
  22.     }
  23. }
  24. ...
  25. t.start("Preparing decorator method access");
  26. AIf d = new ADecorator(a);
  27. t.stop();
  28.  
  29. t.start("Decorator method access");
  30. for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  31.     d.add(i);
  32. }
  33. t.stop();


Выполнилось за 124 мс — получилось даже медленнее, чем FastMethod. И так как каждый хороший программист использует шаблоны проектирования — мы можем сказать, что с некоторой оптимизацией reflection может быть столь же быстрой, как и прямые вызовы без reflection.

Полный листинг:

Copy Source | Copy HTML
  1. package reflection;
  2.  
  3. import net.sf.cglib.reflect.FastClass;
  4. import net.sf.cglib.reflect.FastMethod;
  5.  
  6. import java.lang.reflect.Field;
  7. import java.lang.reflect.InvocationTargetException;
  8. import java.lang.reflect.Method;
  9. import java.util.LinkedHashMap;
  10. import java.util.Map;
  11.  
  12. class Timer {
  13.     private long startTime =  0;
  14.  
  15.     private String msg = null;
  16.  
  17.     private Map<String, Long> map = new LinkedHashMap<String, Long>();
  18.  
  19.     public void start(String msg) {
  20.         if (startTime !=  0) {
  21.             throw new IllegalStateException("Already started");
  22.         }
  23.         startTime = System.currentTimeMillis();
  24.         this.msg = msg;
  25.     }
  26.  
  27.     public void stop() {
  28.         if (startTime ==  0) {
  29.             throw new IllegalStateException("Not started");
  30.         }
  31.         long now = System.currentTimeMillis();
  32.         Long n = map.get(msg);
  33.         if (n == null) {
  34.             n = 0l;
  35.         }
  36.         n += (now - startTime);
  37.         map.put(msg, n);
  38.         startTime =  0;
  39.         msg = null;
  40.     }
  41.  
  42.     public void output() {
  43.         for (String msg : map.keySet()) {
  44.             System.out.println(msg + ": " + map.get(msg));
  45.         }
  46.     }
  47. }
  48.  
  49. class Ref {
  50.     int value;
  51. }
  52.  
  53. interface AIf {
  54.     public void add(int x);
  55. }
  56.  
  57. class A implements AIf {
  58.     public int value =  0;
  59.  
  60.     public void add(int x) {
  61.         value += x;
  62.     }
  63.  
  64.     public void add(Ref ref) {
  65.         value += ref.value;
  66.     }
  67. }
  68.  
  69. class ADecorator implements AIf {
  70.     private AIf a;
  71.  
  72.     public ADecorator(AIf a) {
  73.         this.a = a;
  74.     }
  75.  
  76.     public void add(int x) {
  77.         a.add(x);
  78.     }
  79. }
  80.  
  81. public class Reflect {
  82.     private static final int TOTAL_LOOP_COUNT = 5000000;
  83.  
  84.     /**
         * How many loops to do in one step.
         */
  85.     private static final int LOOPS_IN_STEP_COUNT = 100;
  86.  
  87.     /**
         * How many steps to do - in each step there will be
         * {@link #LOOPS_IN_STEP_COUNT} loops.
         */
  88.     private static final int STEP_COUNT = TOTAL_LOOP_COUNT / LOOPS_IN_STEP_COUNT;
  89.  
  90.     public static void main(String[] args) throws SecurityException,
  91.             NoSuchFieldException, IllegalArgumentException,
  92.             IllegalAccessException, NoSuchMethodException,
  93.             InvocationTargetException {
  94.  
  95.         Timer t = new Timer();
  96.         A a = new A();
  97.  
  98.         for (int j =  0; j < STEP_COUNT; ++j) {
  99.  
  100.             t.start("Direct field access");
  101.             for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  102.                 a.value += i;
  103.             }
  104.             t.stop();
  105.  
  106.             t.start("Preparing for reflective field access");
  107.             Field f = A.class.getField("value");
  108.             t.stop();
  109.  
  110.             t.start("Reflective field access");
  111.             for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  112.                 f.set(a, ((Integer) f.get(a)) + i);
  113.             }
  114.             t.stop();
  115.  
  116.             t.start("Direct method access");
  117.             for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  118.                 a.add(i);
  119.             }
  120.             t.stop();
  121.  
  122.             t.start("Preparing for reflective method access");
  123.             Method m = A.class.getDeclaredMethod("add", int.class);
  124.             t.stop();
  125.  
  126.             t.start("Reflective method access");
  127.             for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  128.                 m.invoke(a, i);
  129.             }
  130.             t.stop();
  131.  
  132.             t.start("Preparing for fast reflective method access");
  133.             FastClass fc = FastClass.create(A.class);
  134.             FastMethod fm = fc.getMethod(m);
  135.             t.stop();
  136.  
  137.             t.start("Fast reflective method access");
  138.             for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  139.                 fm.invoke(a, new Object[]{i});
  140.             }
  141.             t.stop();
  142.  
  143.             t.start("Preparing for fast reflective method access (2)");
  144.             FastClass fc2 = FastClass.create(A.class);
  145.             FastMethod fm2 = fc2.getMethod("add", new Class[]{
  146.                     Ref.class
  147.             });
  148.             Ref ref = new Ref();
  149.             Object[] arguments = new Object[]{ref};
  150.             t.stop();
  151.  
  152.             t.start("Fast reflective method access (2)");
  153.             for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  154.                 ref.value = i;
  155.                 fm2.invoke(a, arguments);
  156.             }
  157.             t.stop();
  158.  
  159.             t.start("Preparing decorator method access");
  160.             AIf d = new ADecorator(a);
  161.             t.stop();
  162.  
  163.             t.start("Decorator method access");
  164.             for (int i =  0; i < LOOPS_IN_STEP_COUNT; ++i) {
  165.                 d.add(i);
  166.             }
  167.             t.stop();
  168.  
  169.         }
  170.  
  171.         t.output();
  172.     }
  173. }
  174.  


И результат:

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
+50
14 сентября 2009, 12:32
56
sedovmik 64,3

комментарии (13)

0
vinni #
Спасибо, с толком, попробую.
0
cococo #
Тема интересная, но слишком специализированная.
Думаю, для большинства людей вот этой статьи будет достаточно:
www.javenue.info/post/84 — это своего рода Reflection Cookbook
0
zabr #
Интересно было бы посмотреть тоже самое на шарпе… :))
Статья отличная!
0
Sane #
На C# подобное реализуется с помощью LambdaExpression.Compile(). (http://stackoverflow.com/questions/346523/how-do-i-compile-an-expression-tree-into-a-callable-method-c как quick-start)
0
sse #
Вероятно, там понадобится DynamicMethod, который и будет вызывать target. Подозреваю, что туда добавится некоторая стоимость копирования аргументов по стеку + 1 callvirt.
0
sse #
Вероятно, там понадобится DynamicMethod, который и будет вызывать target. Подозреваю, что туда добавится некоторая стоимость копирования аргументов по стеку + 1 callvirt.
+4
shai_xylyd #
На шарпе раз, используя System.Reflection.Emit и Dynamic Methods. И два используя C# compiler as a service в моно, и Nemerle's compiler в mono и .Net.
0
zabr #
tnx :))
0
eschava #
надо было замерять не время 5 000 000 вызовов, а сначала прогнать 5 000 000 вызовов без замера, а только потом замерить. чище результаты получатся
+8
KonstantinSolomatov #
Вызовы рефлекшина такие дорогие, потому что проверяется видимость, секьюрити итп вещи. Вызовы можно ускорить до уровня FastClass, итп просто вызвав у метода метод setAccessible(true). Так что, чтобы получать хороший перформанс в рефлекшине, не зачем так извращаться.
0
sedovmik #
отлично! не думал что setAccessible настолько скажется на производительности, спасибо. Вот что получилось:

Preparing for reflective method access: 2683
Reflective method access: 22028
Preparing for reflective accessible method access: 1746
Reflective accessible method access: 725

Preparing for fast reflective method access: 3530
Fast reflective method access: 1692
Preparing for fast reflective method access (2): 10953
Fast reflective method access (2): 588
+11
TheShade #
Привет, есть несколько серьёзных замечаний по методологии (опять даю ссылку на «The Art Of (Java) Benchmarking»):
1. Обязательно выводите значение a.value, чтобы компилятор не убрал «ненужные» операции над a.
2. Обязательно прогревайте код перед тем как начинать измерения, лучше вообще несколько раз вызвать нестатический метод, чтобы не надеяться на On-Stack-Replacement.
3. Обязательно набирайте статистику (минимум 30 итераций), по выборке из одного элемента нельзя ничего путнего сказать.
4. Обязательно указывайте, с какими ключами и на какой платформе сделано измерение.

Вот вам ваша бенчмарка, в которой пофикшены все эти замечания: pastebin.com/m3bfbeea6

На моём Core2Duo 2 Ghz / Sun JDK 1.6.0_14 -server:

Statistics:
145 2 class.getField(...) [preparing for reflective field access]
124 2 class.getField(...) [preparing for reflective field access + setAccessible(true)]
130 2 class.getDeclaredMethod(...) [Preparing for reflective method access]
123 3 class.getDeclaredMethod(...) [Preparing for reflective method access + setAccessible(true)]
102 7 cglib.fastclass.getMethod(...) [Preparing for fast reflective method access]
115 4 cglib.method.invoke(..., args) [Preparing for fast reflective method access (2)]
65 2 d.add(...) [Preparing decorator method access]
-----
67 1 value [Direct field access]
69 2 add(...) [Direct method access]
91 2 d.add(...) [Decorator method access]
7971 19 f.set(f.get(...)) [Reflective field access]
312 3 f.set(f.get(...)) [Reflective field access + setAccessible(true)]
266 3 m.invoke(...) [Reflective method access]
201 2 m.invoke(...) [Reflective method access + setAccessible(true)]
169 2 cglib.method.invoke(...) [Fast reflective method access]
139 2 cglib.method.invoke(..., args) [Fast reflective method access (2)]


Первый столбец — среднее по 30 итерациям, второй — стандартное отклонение.

По этим данным, кстати, видно, что «правильная» рефлексия с setAccessible(true) ненамного медленнее обычного вызова.
0
sedovmik #
Спасибо, по настоящему полезный комментарий. Есть несколько хороших моментов в этой методике. Главный — cglib действительно быстрее reflection, а значит я не зря переводит всё это :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.