2016-06-08 4 views
6

제 직업에는 수학 공식을 지정하는 DSL이 있습니다. 나중에 DSL을 사용하여 많은 포인트 (수백만)를 적용 할 수 있습니다.MethodHandles 또는 LambdaMetafactory?

현재까지 우리는 수식의 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 및 기타 컴포지션 함수 호출로 인해) 매우 길며 평가를 위해 추가 된 속도 향상은 600 만 회 이상의 반복 후에 만 ​​차이를 만들기 시작합니다.

10M 반복에서 메소드 핸들이 실제로 빛나기 시작하지만 수백만 번의 반복은 일반적인 사용 사례가 아닙니다. 우리는 결과가 혼합되는 10k-1M 정도에 가깝습니다.

또한 실제 계산 속도는 빨라지지만 그리 많지는 않습니다 (~ 2-10 회). https://stackoverflow.com/a/19563000/389405

그리고이 시도 시작하는 가려움 해요 : 나는

그래서 어쨌든, 나는 다시 StackOverflow의 수색을 시작,이 같은 LambdaMetafactory 스레드를 보았다 .. 조금 더 빠르게 실행할 수있는 것을 기대하고 있었다. 그러나 그 전에, 나는 몇 가지 질문에 대한 귀하의 의견을 싶습니다

  • 내가 모든 람다를 구성 할 수 있어야합니다. MethodHandles는 그것을하기위한 (천천히, 감탄할 정도로) 많은 방법을 제공합니다. 그러나 lambda는 더 엄격한 "인터페이스"를 가지고 있으며, 어떻게 해야할지에 대해 아직 머리를 쓰지 않습니다. 어떻게 아십니까?

  • lambdas 및 메서드 핸들은 매우 상호 연결되어 있으며 상당한 속도 향상을 얻지 못할지 잘 모르겠습니다. 나는 간단한 람다에 대한 이러한 결과를 보았습니다 : direct: 0,02s, lambda: 0,02s, mh: 0,35s, reflection: 0,40하지만 구성된 람다는 어떨까요?

감사합니다!

+1

이러한 접근 방식을 제안하는 동적 구성 요소가 있습니까? 보통 (바람직하게 불변 인) 객체의 트리는 그렇게 나쁘지 않습니다. – Holger

+0

우리의 오브젝트 트리는 실제로 인터페이스를 사용하며 모든 다이나믹 디스패치의 비용을 논의했으며이를 위해 맞춤형 메서드 핸들을 사용하는 것이 좋습니다. 내 테스트에 따르면 큰 나무 나 작은 나무를 반복하는 경우가 많습니다. 이제 람다가 조금 더 향상시킬 수 있는지 궁금합니다. – Gui13

+1

"모든 다이나믹 디스패치 비용"과 관련하여 실제 벤치 마크를 수행 했습니까? 당신이 불변의 나무를 가지고 있다면, HotSpot은 보통 공격적인 인라이닝을하는데 아주 좋습니다. 또한 람다를 사용할 때도 인터페이스를 중심으로 설계 되었기 때문에 이러한 기능을 사용해야합니다. – Holger

답변

4

대부분의 경우, 특정 인터페이스를 수행하거나 공통 평가자 기본 클래스를 상속하는 노드로 구성된 불변의 평가 트리가 탁월하다고 생각합니다.HotSpot은 적어도 하위 트리에 대해서는 (공격적인) 인라이닝을 수행 할 수 있지만 인라인 할 노드 수를 자유롭게 결정할 수 있습니다.

반대로 전체 트리에 대한 명시 적 코드를 생성하면 JVM의 임계 값을 초과 할 위험이 있으며, 디스패치 오버 헤드가없는 코드가 있지만 항상 해석 될 수 있습니다.

적용된 MethodHandle 트리는 다른 트리와 마찬가지로 시작하지만 더 높은 오버 헤드가 있습니다. 자체 최적화가 핫스팟 자체 인라이닝 전략을 능가 할 수 있는지 여부는 논쟁의 여지가 있습니다. 그리고 당신이 알아 차렸 듯이, 자체 튜닝이 시작되기 전에 호출 횟수가 길어졌습니다. 작성된 메소드 핸들에 대한 임계 값은 불행한 방법으로 축적됩니다.

Pattern.compile을 사용하여 평가 트리 패턴의 한 가지 주요한 이름을 지정하면 메서드 이름이 해당 방향을 잘못 인식 할 수는 있지만 바이트 코드 나 네이티브 코드가 생성되지 않습니다. 내부 표현은 노드의 불변 트리이며, 다른 종류의 조작의 조합을 나타냅니다. 유익한 것으로 간주되는 곳에서 플랫 화 된 코드를 생성하는 것은 JVM 옵티 마이저에 달려 있습니다.

람다 식으로 게임을 변경하지 마십시오. 이 클래스를 사용하면 인터페이스를 구현하고 대상 메서드를 호출하는 클래스를 생성 할 수 있습니다.

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; 
} 

단지 짧은 손으로

참고 예 I 일부를 포함하는 방법을 추가 main. 이 함수는 첫 번째 호출에서 컴파일 된 코드처럼 작동합니다. 실제로는 컴파일 된 코드로만 구성되어 있지만 여러 함수로 구성되어 있습니다.

+0

이것은 좋은 대답입니다. Holger에게 감사드립니다. 나는 여전히 회색 영역 (variadic 양의 매개 변수를 갖는 방법)을 가지고 있지만 좋은 시작이다. – Gui13