web-dev-qa-db-ja.com

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これを最適化してキャッシュを不要にすることができますか?これに関する一般的なベストプラクティスはありますか?そのようなキャッシングが有用であるかどうかに固有の実装?

71
gexicide

同じcall-siteの頻繁な実行と、ステートレスラムダまたはステートフルラムダ、および頻繁な使用method-reference(異なる呼び出しサイトによる)同じメソッドへ。

次の例を見てください。

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

ここでは、同じ呼び出しサイトが2回実行され、ステートレスラムダが生成され、現在の実装は"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");
  }
}

この2番目の例では、同じ呼び出しサイトが2回実行され、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");

対照的に、最後の例では、同等のメソッド参照を生成する2つの異なる呼び出しサイトがありますが、1.8.0_05印刷されます"unshared"および"unshared class"


ラムダ式またはメソッド参照ごとに、コンパイラは、クラスのbootstrapメソッドで提供されるJREを参照するinvokedynamic命令を発行します LambdaMetafactory および生成に必要な静的引数目的のラムダ実装クラス。メタファクトリが生成するものは実際のJREに委ねられますが、最初の呼び出しで作成されたinvokedynamicインスタンスを記憶して再利用するのはCallSite命令の指定された動作です。

現在のJREは、ステートレスラムダの定数オブジェクトに対して ConstantCallSite を含む MethodHandle を生成します(異なる方法で実行する考えられる理由はありません)。また、staticメソッドへのメソッド参照は常にステートレスです。ステートレスラムダと単一の呼び出しサイトの場合、答えは「キャッシュしないでください。JVMが行います。キャッシュしない場合は、対処しないべき強力な理由が必要です。

パラメーターを持つラムダ、およびthis::funcは、thisインスタンスへの参照を持つラムダです。少し異なります。 JREはそれらをキャッシュできますが、これは実際のパラメーター値と結果のラムダとの間にある種のMapを維持することを意味します。現在のJREは、状態を持つラムダインスタンスをキャッシュしません。

しかし、これはラムダクラスが毎回作成されることを意味しません。これは、解決された呼び出しサイトが、最初の呼び出しで生成されたラムダクラスをインスタンス化する通常のオブジェクト構築のように動作することを意味します。

同様のことが、異なる呼び出しサイトによって作成された同じターゲットメソッドへのメソッド参照にも適用されます。 JREはそれらの間で単一のラムダインスタンスを共有できますが、現在のバージョンでは共有されません。これは、おそらくキャッシュメンテナンスが報われるかどうかが明確でないためです。ここでは、生成されたクラスでさえ異なる場合があります。


したがって、あなたの例のようなキャッシングは、プログラムにない場合とは異なることをさせる可能性があります。しかし、必ずしもより効率的ではありません。キャッシュされたオブジェクトは、一時オブジェクトよりも常に効率的ではありません。ラムダの作成によるパフォーマンスへの影響を実際に測定しない限り、キャッシュを追加しないでください。

キャッシングが役立つ可能性がある特別なケースはいくつかあると思います。

  • 同じメソッドを参照する多くの異なる呼び出しサイトについて話している
  • ラムダは、後でuse-site will で使用されるため、コンストラクタ/クラスの初期化で作成されます。
    • 複数のスレッドから同時に呼び出される
    • first呼び出しのパフォーマンスが低下する
71
Holger

私が言語仕様を理解している限り、観察可能な動作を変更しても、この種の最適化が可能です。セクション JSL8§15.13. からの以下の引用を参照してください。

§15.13.3メソッド参照の実行時評価

実行時、メソッド参照式の評価は、通常の完了がオブジェクトへの参照を生成する限り、クラスインスタンス作成式の評価に似ています。 [..]

[..]Either以下のプロパティを持つクラスの新しいインスタンスが割り当てられて初期化されるか、existing以下のプロパティを持つクラスのinstanceが参照されます。

簡単なテストでは、静的メソッドのメソッド参照により、各評価で同じ参照が得られることが示されています。次のプログラムは3行を出力しますが、最初の2行は同じです:

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

非静的関数に対して同じ効果を再現することはできません。ただし、この最適化を妨げる言語仕様には何も見つかりませんでした。

したがって、この手動最適化の値を決定するためのパフォーマンス分析がない限り、それに対して強くお勧めします。キャッシングはコードの可読性に影響し、値があるかどうかは不明です。 早すぎる最適化はすべての悪の根源です。

7
nosid

残念ながら、理想的な状況の1つは、ラムダがリスナーとして渡され、将来のある時点で削除したい場合です。キャッシュされた参照は、別の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を必要としないのは良かったでしょう。

7
user2219808