At my job we have a DSL for specfying mathematical formulas, that we later apply to a lot of points (in the millions).
As of today, we build an AST of the formula, and visit each node to produce what we call an "Evaluator". We then pass that evaluator the arguments of the formula, and for each point it does the computing.
For instance, we have that formula: x * (3 + y)
┌────┐
┌─────┤mult├─────┐
│ └────┘ │
│ │
┌──v──┐ ┌──v──┐
│ x │ ┌───┤ add ├──┐
└─────┘ │ └─────┘ │
│ │
┌──v──┐ ┌──v──┐
│ 3 │ │ y │
└─────┘ └─────┘
Our evaluator will emit "Evaluate" objects for each step.
This method is easy to program, but not very efficient.
So I started looking into method handles to build up a "composed" method handle to speed things up lately.
Something along this: I have my "Arithmetic" class with :
public class Arithmetics {
public static double add(double a, double b){
return a+b;
}
public static double mult(double a, double b){
return a*b;
}
}
And when building my AST I use MethodHandles.lookup() to directly get a handle on those and compose them. Something along these lines, but in a tree:
Method add = ArithmeticOperator.class.getDeclaredMethod("add", double.class, double.class);
Method mult = ArithmeticOperator.class.getDeclaredMethod("mult", double.class, double.class);
MethodHandle mh_add = lookup.unreflect(add);
MethodHandle mh_mult = lookup.unreflect(mult);
MethodHandle mh_add_3 = MethodHandles.insertArguments(mh_add, 3, plus_arg);
MethodHandle formula = MethodHandles.collectArguments(mh_mult, 1, mh_add_3); // formula is f(x,y) = x * (3 + y)
Sadly, I'm quite disapointed by the results. For instance, the actual construction of the method handle is very long (due to calls to MethodHandles::insertArguments and other such compositions functions), and the added speedup for evaluation only starts to make a difference after over 600k iterations.
At 10M iterations, the Method handle starts to really shine, but millions of iterations is not (yet?) a typical use case. We are more around 10k-1M, where the result is mixed.
Also, the actual computation is sped up, but by not so much (~2-10 times). I was expecting the thing to run a bit faster..
So anyway, I started scouring StackOverflow again, and saw the LambdaMetafactory threads like these: https://mcmap.net/q/280633/-faster-alternatives-to-java-39-s-reflection-closed
And I'm itching to start trying this. But before that, I'd like your input on some questions:
I need to be able to compose all those lambdas. MethodHandles provides lots of (slowish, admitedly) ways to do it, but I feel like lambdas have a stricter "interface", and I can't yet wrap my head on how to do that. Do you know how?
lambdas and method handles are quite interconnected, and I'm not sure that I will get a significant speedup. I see these results for simple lambdas:
direct: 0,02s, lambda: 0,02s, mh: 0,35s, reflection: 0,40
but what about composed lambdas?
Thanks guys!
LambdaMetaFactory
is that it creates a new class, which means the JVM optimizes it much more aggressively than methodhandles held in local or instance fields. The only other way to achieve the same effect would be usingUnsafe
to generate anonymous classes. So maybe just keep your MH tree and convert it to a class with the LMF as a final step? If those things are created dynamically you might also want to tune JVM parameters so that the JITs can kick in sooner. They're normally tuned for reaching steady state and then changing little. – AntimacassarLambdaMetaFactory
doesn’t support composed method handles. So you can only generate instances which delegate to an existing method, which implies, that you have to generate the code for combining nodes yourself. This is basically, what my answer is about, once you are aware that you need these methods, you can create the nodes without reflective operations, using the Java 8 source level features. Then, LMF still can be used to integrate particular specialized leaf nodes. – Bowline