Ist das Zwischenspeichern von Methodenreferenzen in Java 8 eine gute Idee?

81

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 hotFunctionwird 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?

Gexizid
quelle
Ich würde sagen, dass Sie diese Funktion so oft verwenden, dass diese Abstimmungsstufe erforderlich / wünschenswert ist. Vielleicht ist es besser, Lambdas fallen zu lassen und die Funktion direkt zu implementieren, was mehr Raum für andere Optimierungen bietet.
SJuan76
@ SJuan76: Da bin ich mir nicht sicher! Wenn eine Methodenreferenz zu einer anonymen Klasse kompiliert wird, ist dies so schnell wie ein normaler Schnittstellenaufruf. Daher denke ich nicht, dass funktionaler Stil für Hot Code vermieden werden sollte.
Gexizid
4
Methodenreferenzen werden mit implementiert 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?
Nosid
@nosid: Nein, habe keinen Vergleich gemacht. Da ich jedoch eine sehr frühe Version von OpenJDK verwende, sind meine Zahlen möglicherweise ohnehin nicht von Bedeutung, da ich denke, dass die erste Version die neuen Funktionen nur schnell und schmutzig implementiert und dies nicht mit der Leistung verglichen werden kann, wenn die Funktionen ausgereift sind im Laufe der Zeit. Ist die Spezifikation wirklich vorgeschrieben, die invokedynamicverwendet werden muss? Ich sehe hier keinen Grund dafür!
Gexizid
4
Es sollte automatisch zwischengespeichert werden (dies entspricht nicht dem Erstellen einer neuen anonymen Klasse jedes Mal), sodass Sie sich nicht um diese Optimierung kümmern müssen.
Assylias

Antworten:

82

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 Runtimedruckt 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_05diesem Zeitpunkt werden "unshared"und gedruckt "unshared class".


Für jeden Lambda-Ausdruck oder jede Methodenreferenz gibt der Compiler eine invokedynamicAnweisung aus, die auf eine von JRE bereitgestellte Bootstrap-Methode in der Klasse LambdaMetafactoryund 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 der invokedynamicAnweisung, CallSitedie beim ersten Aufruf erstellte Instanz zu speichern und wiederzuverwenden .

Die aktuelle JRE erzeugt ein ConstantCallSitea enthält , MethodHandleauf ein konstantes Objekt für staatenlos lambdas (und es gibt keinen denkbaren Grund , es anders zu tun). Und Methodenverweise auf staticMethoden 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::funceinem Lambda, das auf die thisInstanz verweist, sind die Dinge etwas anders. Die JRE darf sie zwischenspeichern, dies würde jedoch bedeuten, dass eine Art Mapzwischen 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:

  • Wir sprechen über viele verschiedene Anrufstellen, die sich auf dieselbe Methode beziehen
  • Das Lambda wird in der Konstruktor- / Klasseninitialisierung erstellt, da dies später auf der Verwendungsseite erfolgt
    • von mehreren Threads gleichzeitig aufgerufen werden
    • leiden unter der geringeren Leistung des ersten Aufrufs
Holger
quelle
5
Klarstellung: Der Begriff "Call-Site" bezieht sich auf die Ausführung der invokedynamicAnweisung, die das Lambda erzeugt. Es ist nicht der Ort, an dem die funktionale Schnittstellenmethode ausgeführt wird.
Holger
1
Ich dachte, dass this-erfassende Lambdas Singletons mit Instanzbereich sind (eine synthetische Instanzvariable für das Objekt). Das ist nicht so?
Marko Topolnik
2
@ Marko Topolnik: Das wäre eine konforme Kompilierungsstrategie, aber nein, ab Oracle JDK 1.8.0_40ist dies nicht der Fall . Diese Lambdas werden nicht erinnert, so dass Müll gesammelt werden kann. invokedynamicDenken 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.
Holger
2
Es scheint keine Standardbibliotheksklasse mit dem Namen zu geben MethodReference. Meinst du MethodHandlehier
Lii
2
@Lii: Du hast recht, das ist ein Tippfehler. Interessanterweise scheint es noch niemand bemerkt zu haben.
Holger
11

Eine 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.

user2219808
quelle
Ah, ich verstehe den Punkt jetzt. Klingt vernünftig, ist aber wahrscheinlich nicht das Szenario, über das das OP spricht. Trotzdem positiv bewertet.
Tagir Valeev
8

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 :

§15.13.3 Laufzeitauswertung von Methodenreferenzen

Zur Laufzeit Auswertung eines Verfahren Referenz Expression ist ähnlich Auswertung einer Klasseninstanz Schaffung Ausdruck, soweit normaler Abschluss erzeugt eine Referenz auf ein Objekt. [..]

[..] Entweder wird eine neue Instanz einer Klasse mit den folgenden Eigenschaften zugewiesen und initialisiert, oder es wird auf eine vorhandene Instanz einer Klasse mit den folgenden Eigenschaften verwiesen.

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.

nosid
quelle