понедельник, 1 декабря 2008 г.

Rhino и Janino для вычисления математических выражений

Для вычисления математических формул в run-time можно воспользоваться script-движком реализующим JSR-223. В Java 1.6 реализация этой спецификации представлена javascript-движоком Rhino. Однако применение целого script-движка для обычного вычисления формул будет довольно неоправданным решением. Мой поиск привел к Janino - это целый фреймворк для динамической компиляции и исполнения java. То есть он позволяет выполнять скрипты на java внутри самой java. Одно из его призваний это компиляция и выполнение различных математических выражений с помощью org.codehaus.janino.ExpressionEvaluator.

Я решил сравнить производительность Rhino и Janino. Для теста выбрал простую формулу: (x-10)*Math.sin(0.1)*2, вычисление которой будет осуществляться 1000000 раз.

package com.blogspot.nkoksharov;

import javax.script.*;

import org.apache.commons.lang.time.StopWatch;
import org.codehaus.janino.ExpressionEvaluator;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import org.testng.annotations.Test;

public class ExpressionTest {

@Test
public void testRhino() throws ScriptException {
ScriptEngineManager scriptManager = new ScriptEngineManager();
ScriptEngine se = scriptManager.getEngineByName("JavaScript");
SimpleBindings bindings = new SimpleBindings();
bindings.put("x", 2);

StopWatch stopWatch = new StopWatch();
stopWatch.start();
for (int i = 0; i < 1000000; i++) {
Double result = (Double) se.eval("(x-10)*Math.sin(0.1)*2", bindings);
}
stopWatch.stop();
System.out.println("JDK Rhino time: " + stopWatch);
}

@Test
public void testRhinoFunc() throws ScriptException, NoSuchMethodException {
ScriptEngineManager scriptManager = new ScriptEngineManager();
ScriptEngine se = scriptManager.getEngineByName("JavaScript");

String script = "function count(x) { return (x-10)*Math.sin(0.1)*2; }";
se.eval(script);

StopWatch stopWatch = new StopWatch();
stopWatch.start();
for (int i = 0; i < 1000000; i++) {
Invocable inv = (Invocable) se;
Double result = (Double) inv.invokeFunction("count", 2);
}
stopWatch.stop();
System.out.println("JDK Rhino func time: " + stopWatch);
}

@Test
public void testNativeRhinoCompiledFunc() {
ContextFactory cf = new ContextFactory();
Context cx = cf.enterContext();
Scriptable scope = cx.initStandardObjects();
Function f = cx.compileFunction(scope, "function count(x) { return (x-10)*Math.sin(0.1)*2; }", null, 0, null);

StopWatch stopWatch = new StopWatch();
stopWatch.start();
for (int i = 0; i < 1000000; i++) {
Double value = (Double) f.call(cx, scope, scope, new Object[] {2});
}
stopWatch.stop();
System.out.println("Mozilla Rhino compiled-func time: " + stopWatch);
}

@Test
public void testJanino() throws Exception {
ExpressionEvaluator ee = new ExpressionEvaluator("(x-10)*Math.sin(0.1)*2",
Double.class, new String[] {"x"}, new Class[] {Integer.class});

StopWatch stopWatch = new StopWatch();
stopWatch.start();
for (int i = 0; i < 1000000; i++) {
Double result = (Double) ee.evaluate(new Object[] {2});
}
stopWatch.stop();
System.out.println("Janino time: " + stopWatch);
}
}

Результат вполне оправдал мои ожидания:

                                 (ч:мм:сс.мс)
JDK Rhino time: 0:01:50.061
JDK Rhino func time: 0:00:59.040
Mozilla Rhino compiled-func time: 0:00:00.344
Janino time: 0:00:00.156

В итоге получается, что скорость выполнения с Rhino использующейся в JDK заметно отстает от Janino, даже не смотря на выполнение через javascript-функцию. Однако, если вы будете использовать реализацию Rhino от Mozilla, с возможностью компиляции в bytecode, то получите лишь 2-х кратное отставание по скорости. И так первенство у Janino, который также компилирует выражения перед выполнением. При этом обращения к методу ExpressionEvaluator.evaluate будут thread-safe.

5 комментариев:

jkff комментирует...

А если в джаваскрипте сначала 1 раз выполнить строчку "f = function(x) {return (x-10)*Math.sin(0.1)*2}", а потом 1000000 раз выполнять код f(2)? Кроме того, может, у rhino есть в API метод для вызова функций без eval, не требующий парсинга?

Никита Кокшаров комментирует...

Спасибо за замечание, я добавил тесты для функции и компилируемой функции. Если вы знаете способ "вызова функций без eval", то напишите пожалуйста мне, буду признателен.

Beholder комментирует...

Версия Rhino, включённая в JDK, не поставляется в компилятром в байт-код, в отличие от полной версии.

Никита Кокшаров комментирует...

http://java.sun.com/javase/6/docs/technotes/guides/scripting/programmer_guide/index.html - да, байт-код компиляцию убрали, из соображений безопасности, но при этом пишут, что задействовали оптимизатор.

Никита Кокшаров комментирует...

Я добавил тест с Rhino от Mozilla