Является ли метод ссылки кэширования хорошей идеей в Java 8?

У меня есть код вроде следующего:

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 может оптимизировать это и сделать кеширование лишним? Есть ли общая передовая практика в этом отношении или эта высокоинтеллектуальная реализация является специфическим для любого кэширования?

Ответ 1

Вы должны провести различие между частыми выполнениями одного и того же сайта-звонка, без учета состояния lambda или полного состояния lambdas и частого использования ссылки на метод с тем же методом (с помощью разных сайтов-звонков).

Посмотрите на следующие примеры:

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

Здесь один и тот же call-сайт выполняется два раза, создавая безстоящую лямбду и текущая реализация печатает "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, что создает meta factory, но это указание поведения команды invokedynamic для запоминания и повторного использования экземпляра CallSite, созданного при первом вызове.

В текущем JRE создается ConstantCallSite, содержащий MethodHandle постоянный объект для апатридов лямбда (и у них нет мыслимой причины сделать это по-другому). И ссылки методов на метод static всегда являются апатридами. Таким образом, для безъядерных lambdas и одиночных сайтов-вызовов ответ должен быть: dont cache, JVM будет делать, и если это не так, у него должны быть веские причины, по которым вы не должны противодействовать.

Для lambdas, имеющего параметры, и this::func является лямбдой, которая имеет ссылку на экземпляр this, все немного отличается. JRE разрешено кэшировать их, но это подразумевает сохранение некоторого вида Map между фактическими значениями параметров и полученной лямбда, которая может быть более дорогостоящей, чем просто создание этого простого структурированного экземпляра лямбда снова. Текущая JRE не кэширует экземпляры lambda, имеющие состояние.

Но это не означает, что класс лямбда создается каждый раз. Это просто означает, что разрешенный вызывающий сайт будет вести себя как обычная конструкция объекта, создающая экземпляр класса лямбда, который был сгенерирован при первом вызове.

Аналогичные вещи применяются к ссылкам метода к одному и тому же методу назначения, создаваемому разными сайтами-контактами. JRE разрешено использовать один экземпляр лямбды между ними, но в текущей версии это не так, скорее всего, потому, что неясно, будет ли обслуживание кэша окупиться. Здесь даже сгенерированные классы могут отличаться.


Таким образом, кэширование, как в вашем примере, может привести к тому, что ваша программа выполняет разные действия, чем без. Но не обязательно более эффективно. Кэшированный объект не всегда более эффективен, чем временный объект. Если вы действительно не измеряете влияние производительности, вызванное созданием лямбда, вы не должны добавлять кеширование.

Я думаю, что есть только некоторые особые случаи, когда кэширование может быть полезно:

  • мы говорим о множестве разных call-сайтов, ссылающихся на тот же метод
  • lambda создается в инициализации конструктора/класса, потому что позже на сайте
    • вызывается несколькими потоками одновременно
    • страдают от более низкой производительности первого вызова

Ответ 2

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

Я не могу воспроизвести тот же эффект для нестатических функций. Тем не менее, я не нашел ничего в спецификации языка, что препятствует этой оптимизации.

Итак, пока нет анализа производительности, чтобы определить ценность этой ручной оптимизации, я категорически против этого. Кэширование влияет на читаемость кода, и неясно, имеет ли оно какое-либо значение. Преждевременная оптимизация - корень всего зла.

Ответ 3

Одна из ситуаций, когда это хороший идеал, к сожалению, заключается в том, что лямбда передается как слушатель, который вы хотите удалить в какой-то момент в будущем. Кэшированная ссылка будет необходима, как передача другой this:: method reference не будет рассматриваться как один и тот же объект при удалении, и оригинал не будет удален. Например:

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 в этом случае.