Bedenken Sie, ich habe Code wie den folgenden:
class Foo {
Y func(X x) {...}
void doSomethingWithAFunc(Function<X,Y> f){...}
void hotFunction(){
doSomethingWithAFunc(this::func);
}
}
Angenommen, das hotFunction
wird sehr oft genannt. Wäre es dann ratsam, zwischenzuspeichern this::func
, vielleicht so:
class Foo {
Function<X,Y> f = this::func;
...
void hotFunction(){
doSomethingWithAFunc(f);
}
}
Nach meinem Verständnis von Java-Methodenreferenzen erstellt die virtuelle Maschine ein Objekt einer anonymen Klasse, wenn eine Methodenreferenz verwendet wird. Das Zwischenspeichern der Referenz würde dieses Objekt also nur einmal erstellen, während der erste Ansatz es bei jedem Funktionsaufruf erstellt. Ist das richtig?
Sollten Methodenreferenzen, die an heißen Stellen im Code erscheinen, zwischengespeichert werden, oder kann die VM dies optimieren und das Caching überflüssig machen? Gibt es eine allgemeine Best Practice dazu oder ist diese hochgradige VM-Implementierung spezifisch, ob ein solches Caching von Nutzen ist?
quelle
invokedynamic
. Ich bezweifle, dass Sie eine Leistungsverbesserung sehen, wenn Sie das Funktionsobjekt zwischenspeichern. Im Gegenteil: Es könnte Compiler-Optimierungen verhindern. Haben Sie die Leistung der beiden Varianten verglichen?invokedynamic
verwendet werden muss? Ich sehe hier keinen Grund dafür!Antworten:
Sie müssen zwischen häufigen Ausführungen derselben Aufrufstelle für zustandsloses Lambda oder zustandsbehaftete Lambdas und der häufigen Verwendung einer Methodenreferenz auf dieselbe Methode (durch verschiedene Aufrufstellen) unterscheiden.
Schauen Sie sich die folgenden Beispiele an:
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"); }
Hier wird dieselbe Aufrufstelle zweimal ausgeführt, wodurch ein zustandsloses Lambda erzeugt wird und die aktuelle Implementierung gedruckt wird
"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"); } }
In diesem zweiten Beispiel wird der gleiche Anruf Ort zweimal ausgeführt wird , eine Lambda enthält einen Verweis auf eine Herstellung
Runtime
druckt Instanz und die aktuellen Implementierung"unshared"
aber"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");
Im letzten Beispiel sind im Gegensatz dazu zwei verschiedene Aufrufstellen, die eine äquivalente Methodenreferenz erzeugen, aber ab
1.8.0_05
diesem Zeitpunkt werden"unshared"
und gedruckt"unshared class"
.Für jeden Lambda-Ausdruck oder jede Methodenreferenz gibt der Compiler eine
invokedynamic
Anweisung aus, die auf eine von JRE bereitgestellte Bootstrap-Methode in der KlasseLambdaMetafactory
und die statischen Argumente verweist, die zum Erzeugen der gewünschten Lambda-Implementierungsklasse erforderlich sind. Es bleibt der tatsächlichen JRE überlassen, was die Meta-Factory erzeugt, aber es ist ein bestimmtes Verhalten derinvokedynamic
Anweisung,CallSite
die beim ersten Aufruf erstellte Instanz zu speichern und wiederzuverwenden .Die aktuelle JRE erzeugt ein
ConstantCallSite
a enthält ,MethodHandle
auf ein konstantes Objekt für staatenlos lambdas (und es gibt keinen denkbaren Grund , es anders zu tun). Und Methodenverweise aufstatic
Methoden sind immer zustandslos. Für zustandslose Lambdas und einzelne Call-Sites muss die Antwort lauten: Nicht zwischenspeichern, die JVM wird es tun, und wenn dies nicht der Fall ist, muss es starke Gründe geben, denen Sie nicht entgegenwirken sollten.Bei Lambdas mit Parametern und
this::func
einem Lambda, das auf diethis
Instanz verweist, sind die Dinge etwas anders. Die JRE darf sie zwischenspeichern, dies würde jedoch bedeuten, dass eine ArtMap
zwischen den tatsächlichen Parameterwerten und dem resultierenden Lambda beibehalten wird, was teurer sein könnte, als nur diese einfache strukturierte Lambda-Instanz erneut zu erstellen. Die aktuelle JRE speichert keine Lambda-Instanzen mit einem Status zwischen.Dies bedeutet jedoch nicht, dass die Lambda-Klasse jedes Mal erstellt wird. Dies bedeutet lediglich, dass sich die aufgelöste Aufrufstelle wie eine gewöhnliche Objektkonstruktion verhält, die die Lambda-Klasse instanziiert, die beim ersten Aufruf generiert wurde.
Ähnliches gilt für Methodenreferenzen auf dieselbe Zielmethode, die von verschiedenen Aufrufstellen erstellt wurden. Die JRE darf eine einzelne Lambda-Instanz zwischen ihnen teilen, in der aktuellen Version jedoch nicht, höchstwahrscheinlich, weil nicht klar ist, ob sich die Cache-Wartung auszahlt. Hier können sich sogar die generierten Klassen unterscheiden.
Beim Caching wie in Ihrem Beispiel kann Ihr Programm also andere Aufgaben ausführen als ohne. Aber nicht unbedingt effizienter. Ein zwischengespeichertes Objekt ist nicht immer effizienter als ein temporäres Objekt. Sofern Sie nicht wirklich die Auswirkungen einer Lambda-Erstellung auf die Leistung messen, sollten Sie kein Caching hinzufügen.
Ich denke, es gibt nur einige Sonderfälle, in denen Caching nützlich sein könnte:
quelle
invokedynamic
Anweisung, die das Lambda erzeugt. Es ist nicht der Ort, an dem die funktionale Schnittstellenmethode ausgeführt wird.this
-erfassende Lambdas Singletons mit Instanzbereich sind (eine synthetische Instanzvariable für das Objekt). Das ist nicht so?1.8.0_40
ist dies nicht der Fall . Diese Lambdas werden nicht erinnert, so dass Müll gesammelt werden kann.invokedynamic
Denken Sie jedoch daran, dass eine einmal verknüpfte Call-Site möglicherweise wie normaler Code optimiert wird, dh, die Escape-Analyse funktioniert für solche Lambda-Instanzen.MethodReference
. Meinst duMethodHandle
hierEine Situation, in der es leider ein gutes Ideal ist, ist, wenn das Lambda als Hörer übergeben wird, den Sie irgendwann in der Zukunft entfernen möchten. Die zwischengespeicherte Referenz wird benötigt, da die Übergabe einer anderen this :: method-Referenz beim Entfernen nicht als dasselbe Objekt angesehen wird und das Original nicht entfernt wird. Zum Beispiel:
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); } } }
Wäre schön gewesen, in diesem Fall kein LambdaRef zu brauchen.
quelle
Soweit ich die Sprachspezifikation verstehe, ermöglicht sie diese Art der Optimierung, auch wenn sie das beobachtbare Verhalten ändert. Siehe die folgenden Zitate aus Abschnitt JSL8 §15.13.3 :
Ein einfacher Test zeigt, dass Methodenreferenzen für statische Methoden (können) für jede Bewertung dieselbe Referenz ergeben. Das folgende Programm druckt drei Zeilen, von denen die ersten beiden identisch sind:
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); } }
Ich kann nicht den gleichen Effekt für nicht statische Funktionen reproduzieren. Ich habe jedoch nichts in der Sprachspezifikation gefunden, was diese Optimierung behindert.
Solange es keine Leistungsanalyse gibt , um den Wert dieser manuellen Optimierung zu bestimmen, rate ich dringend davon ab. Das Caching beeinträchtigt die Lesbarkeit des Codes und es ist unklar, ob er einen Wert hat. Vorzeitige Optimierung ist die Wurzel allen Übels.
quelle