Czy buforowanie referencji metody jest dobrym pomysłem w Javie 8?

Uznaj, że mam następujący kod:

class Foo {

   Y func(X x) {...} 

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

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

}

Przypuśćmy, że {[2] } nazywa się bardzo często. Czy byłoby wskazane, aby buforować this::func, Może Tak:

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

Jeśli chodzi o moje rozumienie odwołań do metod Javy, maszyna wirtualna tworzy obiekt anonimowej klasy, gdy używane jest odwołanie do metody. Tak więc buforowanie referencji spowoduje utworzenie tego obiektu tylko raz, podczas gdy pierwsze podejście utworzy go przy każdym wywołaniu funkcji. Czy to prawda?

Powinien odwołania do metod, które pojawiają się w gorących pozycjach w kodzie, są buforowane czy maszyna wirtualna jest w stanie to zoptymalizować i uczynić buforowanie zbędnym? Czy istnieje ogólna najlepsza praktyka w tym zakresie, czy ta wysoce implementacja maszyn wirtualnych jest specyficzna, czy takie buforowanie jest w ogóle przydatne?

Author: gexicide, 2014-06-01

3 answers

Należy rozróżnić między częstym wykonywaniem tego samego call-site , dla Lambda stateless lub state-full lambda, a częstym używaniem method-reference do tej samej metody (przez różne miejsca wywołania).

Spójrz na następujące przykłady:

    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");
    }

Tutaj, ta sama strona wywołania jest wykonywana dwa razy, tworząc bezstanową lambda, a obecna implementacja wydrukuje "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");
  }
}

W tym drugim przykładzie, ta sama strona wywołania jest wykonywane dwa razy, tworząc lambda zawierającą odniesienie do instancji Runtime, a obecna implementacja wyświetli "unshared", ale "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");

W przeciwieństwie do tego, w ostatnim przykładzie są dwa różne miejsca wywołania produkujące równoważne odniesienie do metody, ale od {[7] } wydrukuje "unshared" i "unshared class".


Dla każdego wyrażenia lambda lub metody odniesienia kompilator będzie emitował instrukcję invokedynamic, która odnosi się do metody Bootstrap dostarczonej przez JRE w klasie LambdaMetafactory oraz argumenty statyczne niezbędne do wytworzenia pożądanej klasy implementacji lambda. Jest to pozostawione do rzeczywistego JRE, co produkuje meta factory, ale jest to określone zachowanie instrukcji invokedynamic, aby zapamiętać i ponownie użyć instancji CallSite utworzonej przy pierwszym wywołaniu.

Obecny JRE produkuje ConstantCallSite zawierające MethodHandle do stałego obiektu dla bezpaństwowych lambdów (i nie ma żadnego powodu, by robić to inaczej). Oraz odniesienia do metody static są zawsze bezpaństwowe. Więc dla bezstanowych lambda i pojedynczych witryn wywołujących odpowiedź musi być: nie buforuj, zrobi to JVM, a jeśli nie, musi mieć silne powody, dla których nie powinieneś przeciwdziałać.

Dla Lambda o parametrach i this::func jest lambda, która ma odniesienie do instancji this, rzeczy są nieco inne. JRE może je buforować, ale oznaczałoby to zachowanie pewnego rodzaju Map pomiędzy rzeczywistymi wartościami parametrów a wynikająca z tego lambda, która może być bardziej kosztowna niż ponowne tworzenie tej prostej strukturalnej instancji lambda. Obecne JRE nie buforuje instancji lambda posiadających stan.

Nie oznacza to jednak, że Klasa lambda jest tworzona za każdym razem. Oznacza to tylko, że rozwiązana strona wywołania będzie zachowywać się jak zwykła konstrukcja obiektu tworząca instancję klasy lambda, która została wygenerowana przy pierwszym wywołaniu.

Podobne rzeczy odnoszą się do metody odniesienia do tego samego celu metoda stworzona przez różne strony wywołania. JRE może współdzielić jedną instancję lambda między sobą, ale w obecnej wersji tak nie jest, najprawdopodobniej dlatego, że nie jest jasne, czy konserwacja pamięci podręcznej się opłaci. Tutaj nawet wygenerowane klasy mogą się różnić.


Więc buforowanie jak w twoim przykładzie może sprawić, że twój program będzie robił inne rzeczy niż bez niego. Ale niekoniecznie bardziej wydajne. Buforowany obiekt nie zawsze jest bardziej wydajny niż obiekt tymczasowy. Chyba że naprawdę mierzysz wpływ wydajności spowodowany tworzeniem lambda, nie powinieneś dodawać żadnego buforowania.

Myślę, że są tylko niektóre specjalne przypadki, w których buforowanie może być przydatne:]}
  • mówimy o wielu różnych stronach wywołania odnoszących się do tej samej metody
  • lambda jest tworzony w konstruktorze / klasie initialize, ponieważ później na use-site będzie
    • być wywoływane przez wiele wątków jednocześnie
    • pierwsza inwokacja
 60
Author: Holger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-04-16 10:07:01

O ile rozumiem specyfikację języka, pozwala to na tego rodzaju optymalizację, nawet jeśli zmienia obserwowalne zachowanie. Zobacz następujące cytaty z sekcji JSL8 §15.13.3:

§15.13.3 ocena czasu pracy referencji metody

W czasie wykonywania, ewaluacja wyrażenia odniesienia do metody jest podobna do ewaluacji wyrażenia stworzenia instancji klasy, o ile zwykłe zakończenie daje odniesienie do obiektu. [..]

[..] alboprzydzielana jest i inicjalizowana nowa instancja klasy z poniższymi właściwościami, albo odwołuje się do istniejącej instancji klasy z poniższymi właściwościami.

Prosty test pokazuje, że odniesienia do metod statycznych (can) prowadzą do tego samego odniesienia dla każdej oceny. Poniższy program wyświetla trzy linie, z których dwie pierwsze są identyczne:

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);
    }
}

I can ' t reproduce the same effect for non-static funkcje. Nie znalazłem jednak w specyfikacji języka niczego, co hamowałoby tę optymalizację.

Tak długo, jak nie ma analiza wydajności aby określić wartość tej ręcznej optymalizacji, zdecydowanie odradzam. Buforowanie wpływa na czytelność kodu i nie jest jasne, czy ma on jakąkolwiek wartość. Przedwczesna optymalizacja jest źródłem wszelkiego zła.

 7
Author: nosid,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-01 21:21:59

Jedną z sytuacji, w której jest to dobry ideał, niestety, jest to, że lambda jest przekazywana jako słuchacz, który chcesz usunąć w pewnym momencie w przyszłości. Buforowane odniesienie będzie potrzebne, ponieważ przekazanie innego odniesienia do metody this:: nie będzie postrzegane jako ten sam obiekt podczas usuwania, a oryginał nie zostanie usunięty. Na przykład:

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);
        }
    }
}
Byłoby miło nie potrzebować lambdaRef w tym przypadku.
 7
Author: user2219808,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-29 21:57:07