Чи є кешування посилань на методи хорошою ідеєю в Java 8?


81

Подумайте, у мене є такий код:

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 специфічна, чи корисне таке кешування?


Я б сказав, що ви використовуєте цю функцію настільки, що такий рівень налаштування необхідний / бажаний, можливо, вам було б краще відмовитися від лямбда і реалізувати функцію безпосередньо, що дає більше місця для інших оптимізацій.
SJuan76

@ SJuan76: Я не впевнений у цьому! Якщо посилання на метод компілюється до анонімного класу, це швидко, як звичайний виклик інтерфейсу. Таким чином, я не думаю, що для гарячого коду слід уникати функціонального стилю.
гексицид

4
Посилання на методи реалізовані за допомогою invokedynamic. Я сумніваюся, що ви побачите підвищення продуктивності шляхом кешування об'єкта функції. Навпаки: це може перешкоджати оптимізації компілятора. Чи порівнювали ви ефективність двох варіантів?
nosid

@nosid: Ні, не робив порівняння. Але я використовую дуже ранню версію OpenJDK, тому мої номери в будь-якому випадку можуть не мати значення, оскільки я думаю, що перша версія реалізує лише нові функції, які швидко `` забруднені '', і це не можна порівняти з продуктивністю, коли функції дозріли через деякий час. Чи справді специфікація invokedynamicвимагає використання? Я не бачу для цього жодної причини!
gexicide

4
Він повинен автоматично кешуватися (це не еквівалентно створенню кожного анонімного класу кожного разу), тому вам не слід турбуватися про цю оптимізацію.
assylias

Відповіді:


83

Ви повинні розрізняти часті виконання одного і того ж сайту виклику , для лямбда або лямбди без стану, і часте використання методу-посилання на один і той же метод (різними сайтами викликів).

Подивіться на такі приклади:

    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вміщуючий a MethodHandleдо константного об'єкта для лямбд без громадянства (і немає жодної підстави робити це інакше). А посилання на staticметоди завжди не мають стану. Отже, для лямбд без громадянства та окремих сайтів викликів відповідь повинна бути такою: не кешуйте, JVM зробить, а якщо цього не стане, вона повинна мати вагомі причини, проти яких вам не слід протидіяти.

Щодо лямбд, що мають параметри, і this::funcце лямбда, яка має посилання на thisекземпляр, справа йде дещо інакше. JRE дозволено кешувати їх, але це означатиме збереження певного роду Mapміж фактичними значеннями параметрів та отриманою лямбда, що може бути дорожчим, ніж просто створення цього простого структурованого екземпляра лямбда знову. Поточний JRE не кешує лямбда-екземпляри, що мають стан.

Але це не означає, що клас лямбда створюється щоразу. Це просто означає, що вирішений виклик-сайт буде поводитися як звичайна конструкція об'єкта, що створює клас лямбда-класу, який був сформований при першому виклику.

Подібні речі застосовуються до посилань на методи до одного і того ж цільового методу, створеного різними сайтами викликів. JRE дозволено ділитися між собою одним лямбда-екземпляром, але в поточній версії цього немає, швидше за все, тому, що незрозуміло, чи окупиться обслуговування кеш-пам’яті. Тут навіть генеровані класи можуть відрізнятися.


Тож кешування, як у вашому прикладі, може змусити вашу програму робити інші речі, ніж без. Але не обов'язково ефективніше. Кешований об'єкт не завжди є більш ефективним, ніж тимчасовий об'єкт. Якщо ви дійсно не вимірюєте вплив на продуктивність, спричинений створенням лямбда, не слід додавати кешування.

Думаю, є лише деякі особливі випадки, коли кешування може бути корисним:

  • ми говоримо про безліч різних сайтів викликів, що посилаються на один і той же метод
  • лямбда створюється в конструкторі / класі, який ініціалізується, оскільки пізніше на сайті використання буде
    • викликатись кількома потоками одночасно
    • страждають від нижчої ефективності першого виклику

5
Пояснення: термін "сайт виклику" стосується виконання invokedynamicінструкції, яка створить лямбда -сигнал . Це не місце, де буде виконуватися метод функціонального інтерфейсу.
Holger

1
Я думав, що this-захоплення лямбд - це одиночні елементи з масштабом екземпляру (синтетична змінна екземпляра на об’єкті). Це не так?
Marko Topolnik

2
@Marko Topolnik: це була б відповідна стратегія компіляції, але ні, станом на jdk Oracle до 1.8.0_40неї це не так. Ці лямбди не запам'ятовуються, тому можна збирати сміття. Але пам’ятайте, що як тільки сайт invokedynamicвиклику буде зв’язаний, він може бути оптимізований як звичайний код, тобто Escape Analysis працює для таких лямбда-екземплярів.
Holger

2
Здається, не існує стандартного бібліотечного класу з іменем MethodReference. Ви маєте на увазі MethodHandleтут?
Lii,

2
@Lii: ти маєш рацію, це друкарська помилка. Цікаво, що, здається, ніхто цього раніше не помічав.
Holger

11

На жаль, одна з ситуацій, коли це хороший ідеал, полягає в тому, що лямбда передається як слухач, якого ви хочете прибрати в якийсь момент у майбутньому. Кешоване посилання буде потрібно, оскільки передача іншого посилання 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.


Ах, зараз я бачу сенс. Звучить розумно, хоча, мабуть, не той сценарій, про який говорить ОП. Тим не менше проголосував.
Тагір Валєєв

9

Наскільки я розумію специфікацію мови, це дозволяє такий тип оптимізації, навіть якщо це змінює спостережувану поведінку. Див. Наступні цитати з розділу 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);
    }
}

Я не можу відтворити той самий ефект для нестатичних функцій. Однак у специфікації мови я не знайшов нічого, що стримує цю оптимізацію.

Отже, поки не існує аналізу продуктивності, щоб визначити цінність цієї ручної оптимізації, я настійно не рекомендую її використовувати. Кешування впливає на читабельність коду, і незрозуміло, чи має воно якесь значення. Передчасна оптимізація - корінь усього зла.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.