1. ホーム
  2. java

[解決済み] Java 8でメソッド参照キャッシュは良いアイデアか?

2023-06-28 22:04:29

質問

以下のようなコードがあるとします。

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のメソッド参照について理解している限りでは、メソッド参照が使用されるとVirtual Machineは匿名クラスのオブジェクトを作成します。したがって、参照をキャッシュするとそのオブジェクトは一度だけ作成されますが、最初のアプローチでは関数を呼び出すたびにオブジェクトが作成されます。これは正しいのでしょうか?

コード内のホットポジションに現れるメソッド参照はキャッシュされるべきでしょうか、それとも VM はこれを最適化し、キャッシュを不要にすることができるでしょうか。このことについて一般的なベスト プラクティスがあるのか、それともそのようなキャッシュが役に立つかどうかは、高度に VM インプリメンテーションに依存するのでしょうか?

どのように解決するのですか?

同じものを頻繁に実行する場合と、同じものを頻繁に実行する場合を区別する必要があります。 コールサイト を頻繁に実行することと、ステートレスラムダやステートフルラムダで メソッド参照 を頻繁に使用することです。

次の例を見てください。

    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" .


ラムダ式やメソッド参照に対して、コンパイラは invokedynamic 命令で、JRE が提供するクラス内のブートストラップメソッドを参照します。 LambdaMetafactory と、目的のラムダ実装クラスを生成するために必要な静的引数を参照する命令です。メタファクトリが何を生成するかは実際のJREに委ねられますが、指定された動作として invokedynamic 命令を記憶し、再利用するために CallSite のインスタンスを記憶して再利用します。

現在の JRE が生成する ConstantCallSite を含む MethodHandle をステートレスラムダ用の定数オブジェクトに変換します (そして、これと異なることをする想像しうる理由はありません)。そしてメソッド参照は static メソッドへの参照は常にステートレスです。したがって、ステートレス ラムダと単一のコールサイトに対する答えは、「キャッシュしないで、JVMはそうするでしょう、そうしないなら、あなたが打ち消すべきでない強い理由があるはずです。

パラメータを持つラムダ、および this::func への参照を持つラムダは this インスタンスへの参照を持つラムダである場合、状況は少し異なります。JRE はそれらをキャッシュすることが許されていますが、これは、ある種の Map を維持する必要があり、単純な構造のラムダインスタンスを再び作成するよりもコストがかかる可能性があります。現在の JRE は、状態を持つラムダ インスタンスをキャッシュしません。

しかし、これはラムダクラスが毎回作成されることを意味するものではありません。解決されたコールサイトが、最初の起動時に生成されたラムダクラスのインスタンスを生成する通常のオブジェクト構築のように動作することを意味しています。

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


つまり、あなたの例のようなキャッシュは、そうでない場合とは異なることをプログラムに行わせるかもしれません。しかし、必ずしもより効率的であるとは限りません。キャッシュされたオブジェクトは、一時的なオブジェクトよりも常に効率的とは限りません。ラムダ生成によるパフォーマンスへの影響を本当に測定していない限り、キャッシュを追加すべきではありません。

キャッシュが有効なのは、特殊なケースだけだと思います。

  • 同じメソッドを参照する多くの異なるコールサイトについて話している。
  • ラムダはコンストラクタ/クラスの初期化で作成されます。
    • 複数のスレッドから同時に呼び出される
    • の低い性能に悩まされる。 最初の 呼び出しの