Rozważ, że mam kod podobny do następującego:
class Foo {
Y func(X x) {...}
void doSomethingWithAFunc(Function<X,Y> f){...}
void hotFunction(){
doSomethingWithAFunc(this::func);
}
}
Załóżmy, że hotFunction
jest to wywoływane bardzo często. Czy byłoby wskazane buforowanie this::func
, może w ten sposób:
class Foo {
Function<X,Y> f = this::func;
...
void hotFunction(){
doSomethingWithAFunc(f);
}
}
O ile rozumiem odwołania do metod w języku Java, maszyna wirtualna tworzy obiekt anonimowej klasy, gdy używane jest odwołanie do metody. Zatem buforowanie odwołania stworzyłoby ten obiekt tylko raz, podczas gdy pierwsze podejście tworzy go przy każdym wywołaniu funkcji. Czy to jest poprawne?
Czy odwołania do metod, które pojawiają się w gorących pozycjach w kodzie, powinny być buforowane, czy też maszyna wirtualna może to zoptymalizować i uczynić buforowanie zbędnym? Czy istnieje ogólna najlepsza praktyka w tym zakresie, czy też to wysoce implementacja maszyn wirtualnych jest specyficzna, czy takie buforowanie jest przydatne?
źródło
invokedynamic
. Wątpię, czy zauważysz poprawę wydajności poprzez buforowanie obiektu funkcji. Wręcz przeciwnie: może to uniemożliwić optymalizację kompilatora. Czy porównałeś wydajność obu wariantów?invokedynamic
użycia? Nie widzę tutaj powodu!Odpowiedzi:
Musisz dokonać rozróżnienia między częstymi wykonaniami tej samej strony wywołań , dla bezstanowych lambda lub stanowych lambd, a częstym użyciem odwołania do metody do tej samej metody (przez różne strony wywołań).
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ą lambdę, a bieżąca implementacja zostanie wydrukowana
"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 witryna wywołania jest wykonywana dwa razy, tworząc lambdę zawierającą odwołanie do
Runtime
instancji, a bieżąca implementacja zostanie wydrukowana"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ą dwie różne witryny wywołań generujące równoważne odwołanie do metody, ale od tego
1.8.0_05
momentu zostaną wydrukowane"unshared"
i"unshared class"
.Dla każdego wyrażenia lambda lub odwołania do metody kompilator wyemituje
invokedynamic
instrukcję, która odwołuje się do metody ładowania początkowego dostarczonej przez środowisko JRE w klasieLambdaMetafactory
oraz statycznych argumentów niezbędnych do utworzenia żądanej klasy implementacji lambda. To, co produkuje fabryka meta, pozostawia faktycznemu środowisku JRE, ale jest to określone zachowanieinvokedynamic
instrukcji, aby zapamiętać i ponownie użyćCallSite
instancji utworzonej przy pierwszym wywołaniu.Bieżące środowisko JRE tworzy obiekt stały
ConstantCallSite
zawierający aMethodHandle
dla bezstanowych lambd (i nie ma żadnego powodu, aby robić to inaczej). A odwołania dostatic
metod są zawsze bezstanowe. Tak więc w przypadku lambd bezstanowych i witryn z pojedynczym wywołaniem odpowiedź musi brzmieć: nie buforuj, JVM zrobi, a jeśli nie, musi mieć mocne powody, których nie powinieneś przeciwdziałać.W przypadku lambd mających parametry i
this::func
jest to lambda, które ma odniesienie dothis
instancji, sytuacja wygląda nieco inaczej. Środowisko JRE może je buforować, ale oznaczałoby to utrzymywanie pewnego rodzajuMap
między rzeczywistymi wartościami parametrów a wynikową lambdą, co mogłoby być bardziej kosztowne niż tylko ponowne utworzenie tej prostej strukturalnej instancji lambda. Bieżące środowisko JRE nie buforuje wystąpień lambda mających stan.Ale to nie znaczy, że klasa lambda jest tworzona za każdym razem. Oznacza to po prostu, że rozwiązana witryna 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 dotyczą odwołań do metod do tej samej metody docelowej utworzonej przez różne witryny wywołań. JRE może współdzielić między sobą jedną instancję lambda, ale w obecnej wersji nie, najprawdopodobniej dlatego, że nie jest jasne, czy utrzymanie pamięci podręcznej się opłaci. Tutaj nawet wygenerowane klasy mogą się różnić.
Zatem buforowanie, jak w twoim przykładzie, może sprawić, że twój program będzie robił inne rzeczy niż bez. Ale niekoniecznie bardziej wydajne. Obiekt buforowany nie zawsze jest bardziej wydajny niż obiekt tymczasowy. O ile naprawdę nie mierzysz wpływu na wydajność spowodowanego przez tworzenie lambda, nie powinieneś dodawać żadnego buforowania.
Myślę, że istnieją tylko specjalne przypadki, w których buforowanie może być przydatne:
źródło
invokedynamic
instrukcji, która utworzy lambdę. Nie jest to miejsce, w którym zostanie wykonana metoda interfejsu funkcjonalnego.this
-capturing lambdy były singletonami o zasięgu instancji (syntetyczna zmienna instancji w obiekcie). Tak nie jest?1.8.0_40
tego nie jest. Te lambdy nie są zapamiętywane, więc mogą być zbierane jako śmieci. Pamiętaj jednak, że po połączeniuinvokedynamic
callsite może zostać zoptymalizowany jak zwykły kod, tj. Escape Analysis działa dla takich instancji lambda.MethodReference
. Masz na myśliMethodHandle
tutaj?Sytuacja, w której jest to dobry ideał, niestety, to sytuacja, gdy lambda jest przekazywana jako słuchacz, którego chcesz usunąć w pewnym momencie w przyszłości. Odwołanie z pamięci podręcznej będzie potrzebne, ponieważ przekazanie innego odwołania this :: do metody 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.
źródło
O ile rozumiem specyfikację języka, pozwala na tego rodzaju optymalizację, nawet jeśli zmienia obserwowalne zachowanie. Zobacz następujące cytaty z sekcji JSL8 §15.13.3 :
Prosty test pokazuje, że odwołania do metod dla metod statycznych (mogą) skutkować tym samym odniesieniem dla każdej oceny. Poniższy program wypisuje trzy wiersze, z których pierwsze dwa 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); } }
Nie mogę odtworzyć tego samego efektu dla funkcji niestatycznych. Jednak w specyfikacji języka nie znalazłem nic, co mogłoby utrudniać tę optymalizację.
Tak więc, o ile nie ma analizy wydajności w celu określenia wartości tej ręcznej optymalizacji, zdecydowanie odradzam to. 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.
źródło