[java] 메소드 참조 캐싱이 Java 8에서 좋은 아이디어입니까?

다음과 같은 코드가 있다고 생각하십시오.

class Foo {

   Y func(X x) {...}

   void doSomethingWithAFunc(Function<X,Y> f){...}

   void hotFunction(){
        doSomethingWithAFunc(this::func);
   }

}

hotFunction매우 자주 호출 된다고 가정합니다 . 그러면 다음 this::func과 같이 캐시하는 것이 좋습니다 .

class Foo {
     Function<X,Y> f = this::func;
     ...
     void hotFunction(){
        doSomethingWithAFunc(f);
     }
}

Java 메서드 참조에 대한 제가 이해하는 한, 가상 머신은 메서드 참조가 사용될 때 익명 클래스의 객체를 생성합니다. 따라서 참조를 캐싱하면 해당 객체가 한 번만 생성되고 첫 번째 접근 방식은 각 함수 호출에서 객체를 생성합니다. 이 올바른지?

코드의 핫 위치에 나타나는 메서드 참조를 캐시해야합니까, 아니면 VM이이를 최적화하고 캐시를 불필요하게 만들 수 있습니까? 이것에 대한 일반적인 모범 사례가 있습니까? 아니면 이러한 캐싱이 사용되는지 여부에 따라 VM 구현이 매우 구체적입니까?



답변

상태 비 저장 람다 또는 상태 저장 람다에 대해 동일한 call-site 의 빈번한 실행과 동일한 메서드 에 대한 메서드 참조 의 빈번한 사용 (다른 호출 사이트 별)을 구분해야합니다.

다음 예를보십시오.

    Runnable r1=null;
    for(int i=0; i<2; i++) {
        Runnable r2=System::gc;
        if(r1==null) r1=r2;
        else System.out.println(r1==r2? "shared": "unshared");
    }

여기에서 동일한 호출 사이트가 두 번 실행되어 상태 비 저장 람다를 생성하고 현재 구현은 "shared".

Runnable r1=null;
for(int i=0; i<2; i++) {
  Runnable r2=Runtime.getRuntime()::gc;
  if(r1==null) r1=r2;
  else {
    System.out.println(r1==r2? "shared": "unshared");
    System.out.println(
        r1.getClass()==r2.getClass()? "shared class": "unshared class");
  }
}

두 번째 예에서는, 동일한 통화 사이트는 참조 함유 람다하여 두 번 실행 Runtime인스턴스 인쇄 될 현재 구현 "unshared"하지만이 "shared class".

Runnable r1=System::gc, r2=System::gc;
System.out.println(r1==r2? "shared": "unshared");
System.out.println(
    r1.getClass()==r2.getClass()? "shared class": "unshared class");

반면에, 마지막 실시 예에서 동등한 방법 참조 제조 개의 다른 통화 사이트이지만 현재로 1.8.0_05그것을 출력한다 "unshared"그리고 "unshared class".


각 람다 표현식 또는 메서드 참조에 대해 컴파일러는 invokedynamic클래스에서 JRE 제공 부트 스트랩 메서드를 참조 하는 명령 LambdaMetafactory과 원하는 람다 구현 클래스를 생성하는 데 필요한 정적 인수를 내 보냅니다. 메타 팩토리가 생성하는 것은 실제 JRE에 맡겨져 있지만 첫 번째 호출에서 생성 된 인스턴스 invokedynamic를 기억하고 재사용 하는 것은 명령 의 지정된 동작입니다 CallSite.

현재 JRE는 상태 비 저장 람다에 대한 상수 객체에 ConstantCallSite포함을 생성합니다 MethodHandle(그리고 다르게 수행 할 상상할 수있는 이유가 없습니다). 메서드에 대한 메서드 참조 static는 항상 상태 비 저장입니다. 따라서 상태 비 저장 람다 및 단일 호출 사이트의 경우 대답은 다음과 같아야합니다. 캐시하지 마십시오. JVM이 수행하고 그렇지 않은 경우 대응해서는 안되는 강력한 이유가 있어야합니다.

매개 변수 this::func가 있고 this인스턴스에 대한 참조가있는 람다의 경우 상황이 약간 다릅니다. JRE는 그것들을 캐싱 할 수 있지만 이것은 Map실제 매개 변수 값과 결과 람다 사이에 일종의 유지를 의미 하므로 단순한 구조화 된 람다 인스턴스를 다시 만드는 것보다 더 많은 비용이들 수 있습니다. 현재 JRE는 상태가있는 람다 인스턴스를 캐시하지 않습니다.

그러나 이것이 람다 클래스가 매번 생성된다는 것을 의미하지는 않습니다. 이는 해결 된 호출 사이트가 첫 번째 호출에서 생성 된 람다 클래스를 인스턴스화하는 일반 개체 구성처럼 동작 함을 의미합니다.

다른 호출 사이트에서 만든 동일한 대상 메서드에 대한 메서드 참조에도 유사한 사항이 적용됩니다. JRE는 이들간에 단일 람다 인스턴스를 공유 할 수 있지만 현재 버전에서는 그렇지 않습니다. 아마도 캐시 유지 관리가 효과가 있는지 여부가 명확하지 않기 때문일 것입니다. 여기에서는 생성 된 클래스도 다를 수 있습니다.


따라서 예제와 같은 캐싱은 프로그램이없는 것과 다른 일을 할 수 있습니다. 그러나 반드시 더 효율적인 것은 아닙니다. 캐시 된 개체가 항상 임시 개체보다 더 효율적인 것은 아닙니다. 람다 생성으로 인한 성능 영향을 실제로 측정하지 않는 한 캐싱을 추가해서는 안됩니다.

캐싱이 유용 할 수있는 몇 가지 특별한 경우 만 있다고 생각합니다.

  • 우리는 동일한 방법을 참조하는 다양한 호출 사이트에 대해 이야기하고 있습니다.
  • 람다는 생성자 / 클래스 초기화에서 생성됩니다. 나중에 사용 사이트에서
    • 여러 스레드에서 동시에 호출
    • 첫 번째 호출의 성능이 저하됨


답변

안타깝게도 이것이 좋은 이상인 상황 중 하나는 람다가 미래의 어느 시점에서 제거하려는 청취자로 전달되는 경우입니다. 다른 this :: method 참조를 전달하면 캐시 된 참조가 필요합니다. 제거시 동일한 객체로 표시되지 않으며 원본도 제거되지 않습니다. 예를 들면 :

public class Example
{
    public void main( String[] args )
    {
        new SingleChangeListenerFail().listenForASingleChange();
        SingleChangeListenerFail.observableValue.set( "Here be a change." );
        SingleChangeListenerFail.observableValue.set( "Here be another change that you probably don't want." );

        new SingleChangeListenerCorrect().listenForASingleChange();
        SingleChangeListenerCorrect.observableValue.set( "Here be a change." );
        SingleChangeListenerCorrect.observableValue.set( "Here be another change but you'll never know." );
    }

    static class SingleChangeListenerFail
    {
        static SimpleStringProperty observableValue = new SimpleStringProperty();

        public void listenForASingleChange()
        {
            observableValue.addListener(this::changed);
        }

        private<T> void changed( ObservableValue<? extends T> observable, T oldValue, T newValue )
        {
            System.out.println( "New Value: " + newValue );
            observableValue.removeListener(this::changed);
        }
    }

    static class SingleChangeListenerCorrect
    {
        static SimpleStringProperty observableValue = new SimpleStringProperty();
        ChangeListener<String> lambdaRef = this::changed;

        public void listenForASingleChange()
        {
            observableValue.addListener(lambdaRef);
        }

        private<T> void changed( ObservableValue<? extends T> observable, T oldValue, T newValue )
        {
            System.out.println( "New Value: " + newValue );
            observableValue.removeListener(lambdaRef);
        }
    }
}

이 경우 lambdaRef가 필요하지 않았 으면 좋았을 것입니다.


답변

언어 사양을 이해하는 한 관찰 가능한 동작을 변경하더라도 이러한 종류의 최적화를 허용합니다. 섹션 JSL8 §15.13.3 에서 다음 인용문을 참조하십시오 .

§15.13.3 메서드 참조의 런타임 평가

런타임에 메서드 참조 식의 평가는 정상적인 완료 개체에 대한 참조를 생성 하는 한 클래스 인스턴스 생성 식의 평가와 유사 합니다. [..]

[…] 어느 다음 특성을 갖는 클래스의 새로운 인스턴스를 할당하고 초기화하거나되어 기존 인스턴스 아래의 특성을 갖는 클래스가 참조된다.

간단한 테스트에서는 정적 메서드에 대한 메서드 참조가 각 평가에 대해 동일한 참조를 생성 할 수 있음을 보여줍니다. 다음 프로그램은 처음 두 줄이 동일한 세 줄을 인쇄합니다.

public class Demo {
    public static void main(String... args) {
        foobar();
        foobar();
        System.out.println((Runnable) Demo::foobar);
    }
    public static void foobar() {
        System.out.println((Runnable) Demo::foobar);
    }
}

비 정적 함수에 대해 동일한 효과를 재현 할 수 없습니다. 그러나이 최적화를 방해하는 언어 사양에서 아무것도 발견하지 못했습니다.

따라서이 수동 최적화의 가치를 결정하기위한 성능 분석 이없는 한 이에 대해 강력히 권고합니다. 캐싱은 코드의 가독성에 영향을 미치며 값이 있는지 확실하지 않습니다. 조기 최적화는 모든 악의 근원입니다.


답변