다음과 같은 코드가 있다고 생각하십시오.
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);
}
}
비 정적 함수에 대해 동일한 효과를 재현 할 수 없습니다. 그러나이 최적화를 방해하는 언어 사양에서 아무것도 발견하지 못했습니다.
따라서이 수동 최적화의 가치를 결정하기위한 성능 분석 이없는 한 이에 대해 강력히 권고합니다. 캐싱은 코드의 가독성에 영향을 미치며 값이 있는지 확실하지 않습니다. 조기 최적화는 모든 악의 근원입니다.