Dynamic Code
I am dealing with a problem where I have to compute the value of boolean expressions like the below:
(c1 && c2) || (c1 && c2) || (c1 && c3) || (c1 && c3)
c1, c2. c3 are boolean. The expression can be any arbitrary expression provided as a text by user, the number of argument i.e. I am using java, so to evaluate this dynamic expression I can use the script engine. Another wild idea is to use ANTR4, which I am not going in use. I would evaluate two options in the terms of speed performance.
1. Using scala's code generation technique.
2. Use Script engine like Nashorn. I could also try Scala script engine.
Create a scala project with the following dependency
name := "CodeGenerator"
version := "0.1"
scalaVersion := "2.11.12"
libraryDependencies += "org.scala-lang" % "scala-compiler" % "2.11.12"
Define a class - Compiler (!not a good name). I would call this call from Java to compile a dynamic expression.
import scala.reflect.runtime.universe._
import scala.tools.reflect.ToolBox
import scala.collection.JavaConverters._
class Compiler {
def compile[A](code: String): (Map[String, Any]) => A = {
val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()
val tree = tb.parse(
s"""
|def wrapper(context: Map[String, Any]): Any = {
| $code
|}
|wrapper _
""".stripMargin)
val f = tb.compile(tree)
val wrapper = f()
wrapper.asInstanceOf[Map[String, Any] => A]
}
def convertArgs(args: java.util.Map[String, Object]) ={
args.asScala.toMap
}
}
Build a jar out of this project and use it in the next part. Let's call the output jar ScalaCompiler.jar.
Create a new java project and add ScalaCompiler.jar as dependency.
Here is the full code of the java class to test the above scala Compiler class.
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import scala.Function1;
import scala.collection.immutable.Map;
import javax.script.*;
import java.util.*;
public class CompileTestApp {
private static List<java.util.Map<String, Object>> generateRandomValues(){
final Random random = new Random();
final List<java.util.Map<String, Object>> result = new ArrayList<>();
for(int i=0;i<1000*1000*10;++i) {
java.util.Map<String, Object> map = new java.util.HashMap<>();
map.put("c1", random.nextBoolean());
map.put("c2", random.nextBoolean());
map.put("c3", random.nextBoolean());
result.add(map);
}
return result;
}
public static void testScala(List<java.util.Map<String, Object>> maps){
com.avalanchio.codegen.Compiler compiler = new com.avalanchio.codegen.Compiler();
final List<scala.collection.immutable.Map<String, Object>> scalaMaps = new ArrayList<>();
for (java.util.Map<String, Object> map : maps) {
scalaMaps.add(compiler.convertArgs(map));
}
String expression = "(c1 && c2) || (c1 && c2) || (c1 && c3) || (c1 && c3)";
for(int i=1;i<=3;++i) {
expression = expression.replaceAll("c" + i, "context(\"c" + i + "\").asInstanceOf[Boolean]");
}
final long millis = System.currentTimeMillis();
final Function1<Map<String, Object>, Object> compiled = compiler.compile(expression);
System.out.println("Scala compile time: " + (System.currentTimeMillis() - millis));
final long start = System.currentTimeMillis();
for (scala.collection.immutable.Map<String, Object> scalaMap : scalaMaps) {
final Object apply = compiled.apply(scalaMap);
}
final long duration = System.currentTimeMillis() - start;
System.out.println("Scala duration: " + duration);
}
public static void testNashorn(List<java.util.Map<String, Object>> maps) throws ScriptException {
ScriptEngine engine = new ScriptEngineManager().getEngineByName( "Nashorn" );
final Compilable compilable = (Compilable) engine;
String javaScript = "function evalBoolean(arg){" +
"var c1 = arg.get('c1');" +
"var c2 = arg.get('c2');" +
"var c3 = arg.get('c3');" +
"return (c1 && c2) || (c1 && c2) || (c1 && c3) || (c1 && c3)" +
"};";
javaScript += "function(){return {'evalBoolean': evalBoolean}}";
final CompiledScript compiledScript = compilable.compile(javaScript);
final ScriptObjectMirror scriptObjectMirror = (ScriptObjectMirror) compiledScript.eval();
final ScriptObjectMirror functionTable = (ScriptObjectMirror) scriptObjectMirror.call(null);
final String[] functionNames = functionTable.getOwnKeys(true);
System.out.println( "Function names: " + Arrays.toString( functionNames ) );
final long start = System.currentTimeMillis();
for (java.util.Map<String, Object> map : maps) {
final Object o = functionTable.callMember("evalBoolean", map);
}
System.out.println("Nashorn: " + (System.currentTimeMillis() - start));
}
public static void main(String... args) throws ScriptException {
final List<java.util.Map<String, Object>> maps = generateRandomValues();
testScala(maps);
testNashorn(maps);
}
}
Here is the outcome. For 10 million set of values for (c1, c2, c3), scala option ~13 times faster that nashorn option.
Scala compile time: 63969
Scala duration: 743 ms
Nashorn duration: 10153 ms
Future work:
Rather than creating a seperate scala project, scala code could be used using Scala script engine. Test the performance using Scala script engine. Similar test to be performance for jython engine.