Разбор выражений, bytecode-way

JAVA*
Приходилось ли вам разбирать выражение? Рисовать график функции по введенной с клавиатуры пользователем строке?

Согласитесь, занятие приносит больше головной боли, чем радости от результата. Возможно вы знакомы с библиотеками antlr или javacc, тогда вы отделаетесь малой кровью. Но приобретете хвост из некрасивых сгенерированных классов, который как можно быстрее скроете от посторонних глаз в самом дальнем пакете.

Написав вчера о cglib, я заметил в документации главу о модификации байт-кода. И само собой напрашивается вопрос, а можно ли в runtime заставить класс выполнять, то что очень хочется, а не то что хочет класс?


Я честно взялся за изучение документации и быстро вышел на популярный редактор байт-кода asm. Однако к моему разочарованию модифицировать байт-код можно только неплохо зная и разбираясь в нём. Точнее это означает, что если вам необходимо вернуть результат выполнения «x * x + x /2» — вы должны скомпилировать это выражение в инструкции.

Не желая писать компилятор, я всё-таки сформулировал запрос к google и нашёл, то что искал

К моему счастью подобный компилятор уже реализован в другом редакторе байт-кода javassist. К слову, javassist более документирован, однако в open-source community бытует мнение, что cglib много быстрее. В нашем случае скорость создания класса, не такая такая важная составляющая, как скорость выполнения его методов. Итак, что же требуется сделать:

Copy Source | Copy HTML
  1. public interface Evaluator {
  2.     public double eval(double x);
  3. }


Создание класса:

Copy Source | Copy HTML
  1. ClassPool pool = ClassPool.getDefault();
  2.  
  3. CtClass evalClass = pool.makeClass("Formula");
  4. evalClass.setInterfaces(
  5.         new CtClass[]{
  6.                 pool.makeClass("com.micro.bench.Evaluator")
  7.         });
  8.  
  9. String expession = "x * x + x / 2";
  10.  
  11. evalClass.addMethod(
  12.         CtNewMethod.make(
  13.                 "public double eval (double x) { return (" + expession + ") ; }",
  14.                 evalClass));
  15.  
  16. Class clazz = evalClass.toClass();
  17. runtime = (Evaluator) clazz.newInstance();


Всё! В наших руках объект класса, реализующий интерфейс Evaluator, который вернёт нам x * x + x / 2

Что ж, теперь не плохо было бы сравнить производительность. Спасибо TheShade за комментарий, я немного модифицировал Timer, и использовал его для тестирования. Результат тестирования, показывающий, что оба метода выполняются за одно и тоже время:

     total|    amount|      last|    last 5|   last 10|       avg|       dev|         operation
   6856.00|     20.00|    351.00|    343.00|    341.00|    342.80|      1.24|      .. runtime calc
   6874.00|     20.00|    337.00|    343.00|    344.00|    343.70|      1.03|      .. compile-time calc


  • total — всего мс
  • amount — количество запусков шт
  • last — последний мс
  • last 5 — среднее по последним 5 запускам мс
  • last 10 — среднее по последним 10 запускам мс
  • avg — среднее вообще мс
  • dev — отклонение

Класс Evaluation.java — Сравнение двух способов
Класс Timer.java — Замер скорости и статистика
+16
15 сентября 2009, 16:34
16
sedovmik 64,3

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

+3
isapioff #
немного «более convinient» метод — это использование Expression Language (то, что используется на jsp).
Вот реализация: juel.sourceforge.net/
+1
dmmm #
Согласен на счет ASM. Что бы на нем генерить классы или хотя бы их трансформировать реально приходится кодить в инструкциях байткода. Получается сначала пишешь на обычном текстовом java класс в том виде в котором хочешь, потом декомпилируешь с помощью специальной тулзы в код на ASM и вставляешь это уже в код который в рантайме генерит/модифицирует классы.
+2
just_vladimir #
Я бы попытался использовать для этой задачи Groovy.
0
xflower #
Groovy зело нетороплив.
И хочется строгой типизации.
0
just_vladimir #
Ну мне для большинства задач хватает скорости Groovy, а на счет типизации, то никто не мешает писать на Groovy в строго типизированном виде.
ЗЫ: автор, если не сложно, то добавь в замеры производительности вариант с использованием Groovy.
0
sedovmik #
видимо напрашивается вторая часть — script-way vs bytecode-way.
Раз уж в этом треде обсуждали groovy — может состряпаете пример? Ибо я не в зуб ногой.
0
just_vladimir #
Groovy компилируется в байт код, так что это тоже bytecode-way :)
Хорошо, с меня пример, только по позже.
0
stas_agarkov #
попробуйте nailgun
0
david_mz #
Вы, фактически, компилируете байткод из исходника. Если устраивает, что класс генерится время порядка секунды — то можно и так, конечно. Но если скорость _настолько_ неважна, то гораздо проще использовать встроенный JS-интерпретатор.
0
sedovmik #
А можно пример?
+1
david_mz #
Пример чего, работы с JS?

java.sun.com/javase/6/docs/technotes/guides/scripting/index.html

И гугл по словам “Java scripting API”.
0
sedovmik #
Если интересно, я добавил третьим методом вычисление с помощью js. ужас — мягко сказано. в тесте я складываю результат выполнения функции в цикле. на тестах из статьи — приблизительно 350 мс на 100 миллионов вызовов. Для того чтобы дождаться выполнение теста с js — пришлось уменьшить число прогонов в 1000 раз.

Вышло 4с на 100 тысяч раз. Итого в 11 тысяч раз медленнее, если я правильно посчитал. Вот уж производительность должна быть действительно неважна

Statistics: 
     total|    amount|      last|    last 5|   last 10|       avg|       dev|         operation
      4.00|      8.00|      0.00|      0.00|      0.00|      0.50|      0.29|      .. runtime calc
      2.00|      8.00|      1.00|      0.00|      0.00|      0.25|      0.17|      .. compile-time calc
  30504.00|      8.00|   3913.00|   3698.00|   3050.00|   3813.00|    182.28|      .. script calc
0
david_mz #
Вы проверьте время генерации своего кода из постинга…

В общем, если производительность важна — надо учить джавский ассемблер, ничего не поделаешь.
0
sedovmik #
Ничего вы не поняли, учить ничего не надо. Мораль была такая: javassist компилирует в runtime не хуже javac, и результаты доказывали это.

И чтобы расставить все точки над i — результат с замером скорости генерации. На среднее время генерации очень сильно повлиял первый запуск, когда погружались все классы. Было 108 мс у javassist и 27 мс у script engine (кстати это мало что говорит, могут отметить профессионалы microbenchmark'инга). Но дальше видно, что разницы принципиальной нет.

tatistics: 
     total|    amount|      last|    last 5|   last 10|       avg|       dev|         operation
    111.00|     20.00|      0.00|      0.00|      0.00|      5.55|      5.53|      .. runtime initialization
      0.00|     20.00|      0.00|      0.00|      0.00|      0.00|      0.00|      .. compile time initialization
    100.00|     20.00|      4.00|      3.00|      3.00|      5.00|      1.20|      .. script initialization
   1883.00|     20.00|     84.00|     85.00|     90.00|     94.15|      4.58|      .. runtime calc x 10000000
   1682.00|     20.00|     91.00|     88.00|     88.00|     84.10|      1.91|      .. compile-time calc x 10000000
   9750.00|     20.00|    433.00|    416.00|    420.00|    487.50|     59.32|      .. script calc x 10000
0
david_mz #
Хм. Я, конечно, могу ошибаться, но когда я примерно год назад экспериментировал с этими вещами, классы у меня генерились очень долго, сотни миллисекунд. И javassist я тоже щупал… Но возможно, я что-то делал и не так, раз у Вас совершенно другие цифры получаются…
+1
Jabberwocky #
Можно попробовать использовать javac через Compiler API javax.tools.*
А вот и ссылочка на пример

0
malkolm #
На каждое выражение создавать новый класс? Не слишком ли ресурсоемко?
0
sedovmik #
Зависит от задачи, если требуется несколько раз создать и много больше раз выполнить — тогда всё окей. Если много раз создать и по одному разу вычислить — не очень.
0
MzMz #
А не распухает ли при этом PermGen — вы проверяли?
0
Konstantin0Scheglov #
Конечно распухает, класс-то нужно хранить.
Если это проблема — нужно использовать отдельный ClassLoader и «потерять» его, когда эти классы/выражения станут не нужны.
–1
malkolm #
Это понятно. По мне так сомнительный это подход чтобы выполнять выражения.

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