在我的工作中,我们有一个用于指定数学公式的DSL,我们后来应用于很多点(数百万)。
截至今天,我们构建公式的AST,并访问每个节点以生成我们称之为" Evaluator"。然后,我们将该公式的参数传递给该评估器,并为每个点进行计算。
例如,我们有这个公式:x * (3 + y)
┌────┐
┌─────┤mult├─────┐
│ └────┘ │
│ │
┌──v──┐ ┌──v──┐
│ x │ ┌───┤ add ├──┐
└─────┘ │ └─────┘ │
│ │
┌──v──┐ ┌──v──┐
│ 3 │ │ y │
└─────┘ └─────┘
我们的评估员将发出"评估"每个步骤的对象。
这种方法易于编程,但效率不高。
所以我开始研究方法句柄来构建一个"组合"方法处理最近加快了速度。
这方面的事情:我有我的"算术"上课:
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;
}
}
在构建我的AST时,我使用MethodHandles.lookup()直接获取它们的句柄并组合它们。沿着这些方向的东西,但在一棵树上:
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)
可悲的是,我对结果感到非常失望。
例如,方法句柄的实际构造非常长(由于调用了MethodHandles :: insertArguments和其他此类组合函数),并且评估的增加加速仅在超过600k迭代后开始产生差异。
在10M迭代中,Method句柄开始真正闪耀,但数百万次迭代不是(还是?)典型的用例。我们更多的是10k-1M,结果是混合的。
此外,实际计算加快了,但不是那么多(约2-10倍)。我期待这件事能跑得更快..
所以无论如何,我再次开始搜索StackOverflow,并看到LambdaMetafactory线程如下:https://stackoverflow.com/a/19563000/389405
我很想开始尝试这个。但在此之前,我希望您对某些问题有所了解:
我需要能够组成所有这些lambdas。 MethodHandles提供了许多(缓慢的,令人愉快的)方法,但我觉得lambdas有一个更严格的界面",我还不能理解如何做到这一点。你知道吗?
lambda和方法句柄相互关联,我不确定我会获得显着的加速。我看到简单lambdas的这些结果:direct: 0,02s, lambda: 0,02s, mh: 0,35s, reflection: 0,40
但是组合lambdas怎么样?
谢谢你们!
答案 0 :(得分:5)
我认为,对于大多数实际情况,由实现特定接口或从公共评估程序基类继承的节点组成的不可变评估树是无与伦比的。 HotSpot能够执行(激进的)内联,至少对于子树,但可以自由决定它将内联多少个节点。
相反,为整个树生成显式代码会增加超出JVM阈值的风险,那么,您的代码肯定没有调度开销,但可能会一直运行解释。
一棵适应MethodHandle
的树像其他树一样开始,但开销较高。它自己的优化是否能够击败HotSpots自己的内联策略,值得商榷。正如你所注意到的那样,在进行自我调整之前需要进行大量的调用。看起来,阈值会以不幸的方式累积到组合方法句柄中。
要命名评估树模式的一个突出示例,当您使用Pattern.compile
准备正则表达式匹配操作时,不会生成字节码或本机代码,尽管方法的名称可能会误导进入该方向。内部表示只是一个不可变的节点树,表示不同类型的操作的组合。 JVM优化器可以为它生成扁平代码,因为它被认为是有益的。
Lambda表达式不会改变游戏。它们允许您生成(小)类来完成接口并调用目标方法。您可以使用它们来构建不可变的评估树,虽然这不太可能具有与明确编程的评估节点类不同的性能,但它允许更简单的代码:
public class Arithmetics {
public static void main(String[] args) {
// x * (3 + y)
DoubleBinaryOperator func=op(MUL, X, op(ADD, constant(3), Y));
System.out.println(func.applyAsDouble(5, 4));
PREDEFINED_UNARY_FUNCTIONS.forEach((name, f) ->
System.out.println(name+"(0.42) = "+f.applyAsDouble(0.42)));
PREDEFINED_BINARY_FUNCTIONS.forEach((name, f) ->
System.out.println(name+"(0.42,0.815) = "+f.applyAsDouble(0.42,0.815)));
// sin(x)+cos(y)
func=op(ADD,
op(PREDEFINED_UNARY_FUNCTIONS.get("sin"), X),
op(PREDEFINED_UNARY_FUNCTIONS.get("cos"), Y));
System.out.println("sin(0.6)+cos(y) = "+func.applyAsDouble(0.6, 0.5));
}
public static DoubleBinaryOperator ADD = Double::sum;
public static DoubleBinaryOperator SUB = (a,b) -> a-b;
public static DoubleBinaryOperator MUL = (a,b) -> a*b;
public static DoubleBinaryOperator DIV = (a,b) -> a/b;
public static DoubleBinaryOperator REM = (a,b) -> a%b;
public static <T> DoubleBinaryOperator op(
DoubleUnaryOperator op, DoubleBinaryOperator arg1) {
return (x,y) -> op.applyAsDouble(arg1.applyAsDouble(x,y));
}
public static DoubleBinaryOperator op(
DoubleBinaryOperator op, DoubleBinaryOperator arg1, DoubleBinaryOperator arg2) {
return (x,y)->op.applyAsDouble(arg1.applyAsDouble(x,y),arg2.applyAsDouble(x,y));
}
public static DoubleBinaryOperator X = (x,y) -> x, Y = (x,y) -> y;
public static DoubleBinaryOperator constant(double value) {
return (x,y) -> value;
}
public static final Map<String,DoubleUnaryOperator> PREDEFINED_UNARY_FUNCTIONS
= getPredefinedFunctions(DoubleUnaryOperator.class,
MethodType.methodType(double.class, double.class));
public static final Map<String,DoubleBinaryOperator> PREDEFINED_BINARY_FUNCTIONS
= getPredefinedFunctions(DoubleBinaryOperator.class,
MethodType.methodType(double.class, double.class, double.class));
private static <T> Map<String,T> getPredefinedFunctions(Class<T> t, MethodType mt) {
Map<String,T> result=new HashMap<>();
MethodHandles.Lookup l=MethodHandles.lookup();
for(Method m:Math.class.getMethods()) try {
MethodHandle mh=l.unreflect(m);
if(!mh.type().equals(mt)) continue;
result.put(m.getName(), t.cast(LambdaMetafactory.metafactory(
MethodHandles.lookup(), "applyAsDouble", MethodType.methodType(t),
mt, mh, mt) .getTarget().invoke()));
}
catch(RuntimeException|Error ex) { throw ex; }
catch(Throwable ex) { throw new AssertionError(ex); }
return Collections.unmodifiableMap(result);
}
}
这就是为java.lang.Math
中的基本算术运算符和函数组成的表达式组成评估者所需的一切,后者是动态收集的,用于解决问题的这一方面。
从技术上讲,请注意
public static DoubleBinaryOperator MUL = (a,b) -> a*b;
只是
的简写public static DoubleBinaryOperator MUL = Arithmetics::mul;
public static double mul(double a, double b){
return a*b;
}
我添加了一个包含一些示例的main
方法。请记住,这些函数在第一次调用时就像编译代码一样,事实上,它们只由编译代码组成,但由多个函数组成。