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


81

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

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 구현이 매우 구체적입니까?


이 기능을 너무 많이 사용하여이 수준의 조정이 필요하거나 바람직하다고 말하고 싶습니다. 아마도 람다를 삭제하고 함수를 직접 구현하는 것이 더 나을 것입니다. 이렇게하면 다른 최적화를위한 더 많은 공간이 제공됩니다.
SJuan76

@ SJuan76 : 나는 이것에 대해 확실하지 않다! 메서드 참조가 비정규 클래스로 컴파일되면 일반적인 인터페이스 호출만큼 빠릅니다. 따라서 핫 코드에 대해 기능적 스타일을 피해야한다고 생각하지 않습니다.
gexicide 2014-06-01

4
메서드 참조는 invokedynamic. 함수 객체를 캐싱함으로써 성능 향상을 볼 수 있을지 의문입니다. 반대로 컴파일러 최적화를 방해 할 수 있습니다. 두 변형의 성능을 비교 했습니까?
nosid

@nosid : 아니요, 비교하지 않았습니다. 그러나 저는 OpenJDK의 매우 초기 버전을 사용하고 있으므로 첫 번째 버전은 새로운 기능을 신속하게 구현하고 기능이 성숙했을 때의 성능과 비교할 수 없다고 생각하므로 어쨌든 내 숫자는 중요하지 않을 수 있습니다. 시간이 지남에. 사양이 실제로 invokedynamic사용해야 하는 것을 요구합니까 ? 여기에 대한 이유가 없습니다!
gexicide 2014-06-01

4
자동으로 캐시되어야하므로 (매번 새로운 익명 클래스를 생성하는 것과 같지 않음) 최적화에 대해 걱정할 필요가 없습니다.
assylias

답변:


83

상태 비 저장 람다 또는 상태 저장 람다에 대해 동일한 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는 이들간에 단일 람다 인스턴스를 공유 할 수 있지만 현재 버전에서는 그렇지 않습니다. 아마도 캐시 유지 관리가 효과가 있는지 여부가 명확하지 않기 때문일 것입니다. 여기에서는 생성 된 클래스도 다를 수 있습니다.


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

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

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

5
설명 : "호출 사이트"라는 용어 invokedynamic는 람다를 생성하는 명령 의 실행을 나타냅니다 . 기능적 인터페이스 메소드가 실행되는 곳이 아닙니다.
Holger

1
나는 this-capturing lambdas가 인스턴스 범위의 싱글 톤 (객체의 합성 인스턴스 변수) 이라고 생각했습니다 . 그렇지 않나요?
Marko Topolnik 2014

2
@Marko Topolnik : 그것은 호환되는 컴파일 전략 이겠지만 오라클의 jdk 1.8.0_40에서는 그렇지 않습니다. 이러한 람다는 기억되지 않으므로 가비지 수집 될 수 있습니다. 그러나 일단 invokedynamic호출 사이트가 연결되면 일반 코드처럼 최적화 될 수 있습니다. 즉, 이스케이프 분석은 이러한 람다 인스턴스에 대해 작동합니다.
Holger

2
라는 표준 라이브러리 클래스가없는 것 같습니다 MethodReference. MethodHandle여기 를 의미 합니까?
Lii

2
@Lii : 당신이 맞아요, 그건 오타입니다. 흥미롭게도 아무도 전에 눈치 채지 못한 것 같습니다.
Holger

11

안타깝게도 이것이 좋은 이상인 상황 중 하나는 람다가 미래의 어느 시점에서 제거하려는 청취자로 전달되는 경우입니다. 다른 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가 필요하지 않았 으면 좋았을 것입니다.


아, 이제 요점이 보입니다. OP가 말하는 시나리오는 아니지만 합리적으로 들립니다. 그럼에도 불구하고 찬성했습니다.
Tagir Valeev 2015 년

9

언어 사양을 이해하는 한 관찰 가능한 동작을 변경하더라도 이러한 종류의 최적화를 허용합니다. 섹션 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);
    }
}

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

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.