Pull to refresh

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

Reading time 12 min
Views 50K
Original author: Irek Matysiewicz
Большинство программистов знают о 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.     /**<br/>     * How many loops to do in one step.<br/>     */
  85.     private static final int LOOPS_IN_STEP_COUNT = 100;
  86.  
  87.     /**<br/>     * How many steps to do - in each step there will be<br/>     * {@link #LOOPS_IN_STEP_COUNT} loops.<br/>     */
  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
Tags:
Hubs:
+50
Comments 15
Comments Comments 15

Articles