在我的工作中,我们有一个用于指定数学公式的DSL,后来将其应用于许多点(以百万计)。

从今天开始,我们建立公式的AST,并访问每个节点以产生我们称为“评估者”的信息。然后,我们向该评估者传递公式的参数,并针对每个点进行计算。

例如,我们有一个公式: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和其他此类组合函数的调用),并且增加的求值加速仅在经过60万次迭代后才开始有所作为。

在10M次迭代中,“方法”句柄开始真正发挥作用,但是数百万次迭代(不是吗)不是典型的用例。我们大约在10k-1M左右,结果好坏参半。

此外,实际计算速度加快了,但幅度不大(约2-10倍)。我原以为事情会快一点..

因此,无论如何,我再次开始搜寻StackOverflow,并看到了如下LambdaMetafactory线程:https://stackoverflow.com/a/19563000/389405

我很想开始尝试这个。但在此之前,我希望您提供一些问题的意见:
  • 我需要能够编写所有这些lambda。 MethodHandles提供了很多(缓慢的,公认的)方法来执行此操作,但是我感觉到lambda具有更严格的“接口(interface)”,而且我还无法确定该怎么做。你知不知道怎么?
  • lambdas和方法句柄之间是相互联系的,我不确定是否会获得显着的加速。对于简单的lambda,我看到了以下结果:direct: 0,02s, lambda: 0,02s, mh: 0,35s, reflection: 0,40,但是组合lambda呢?

  • 谢谢你们!

    最佳答案

    我认为,在大多数实际情况下,由满足特定接口(interface)或从通用评估器基类继承的节点组成的不可变评估树是无与伦比的。 HotSpot至少能够对子树执行(积极的)内联,但可以自由决定要内联多少个节点。

    相反,为整个树生成显式代码会带来超过JVM阈值的风险,因此,您的代码肯定没有调度开销,但是可能始终运行。

    匹配的MethodHandle的树像任何其他树一样开始,但是开销更高。其自身的优化是否能够击败HotSpots自身的内联策略,尚有待商.。而且,您已经注意到,在进行自我调整之前,需要进行大量的调用。看来,对于组合方法句柄,阈值会以不幸的方式积累。

    举一个评估树模式的突出例子,当您使用 Pattern.compile 进行正则表达式匹配操作时,尽管该方法的名称可能会误导该方向,但不会生成字节码或 native 代码。内部表示只是节点的不可变树,表示不同类型的操作的组合。在有利的情况下,取决于JVM优化器为其生成扁平化代码。

    Lambda表达式不会改变游戏规则。它们使您可以生成(小的)类,以实现接口(interface)并调用目标方法。您可以使用它们来构建不可变的评估树,尽管这与显式编程的评估节点类不太可能具有不同的性能,但它允许使用更简单的代码:

    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方法,其中包含一些示例。请记住,这些函数在第一次调用时就表现得像编译后的代码,因为实际上它们仅由编译后的代码组成,但由多个函数组成。

    07-26 04:22