82

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

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

4

3 に答える 3

90

ステートレス ラムダまたはステートフル ラムダに対して同じcall-siteを頻繁に実行することと、同じメソッドへのメソッド参照を (異なる呼び出しサイトによって) 頻繁に使用することを区別する必要があります。

次の例を見てください。

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

Runtimeこの 2 番目の例では、同じ call-site が 2 回実行され、インスタンスへの参照を含むラムダが生成され、現在の実装では"unshared"but が出力され"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"andが出力され"unshared class"ます。


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

現在の JRE は、ステートレス ラムダの定数オブジェクトへのConstantCallSite格納を生成します (これを別の方法で行う理由は考えられません)。MethodHandleまた、メソッドへのメソッド参照staticは常にステートレスです。したがって、ステートレスなラムダと単一の呼び出しサイトの場合、答えは次のとおりです。キャッシュしないでください。JVM が行います。そうでない場合は、対抗してはならないという強い理由が必要です。

パラメーターを持つthis::funcラムダで、インスタンスへの参照を持つラムダのthis場合、状況は少し異なります。JRE はそれらをキャッシュすることができますが、これは、実際のパラメーター値と結果のラムダの間に何らかの種類を維持することを意味しMap、その単純な構造化されたラムダ インスタンスを再度作成するよりもコストがかかる可能性があります。現在の JRE は、状態を持つラムダ インスタンスをキャッシュしません。

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

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


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

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

  • 同じメソッドを参照する多くの異なる呼び出しサイトについて話している
  • ラムダはコンストラクター/クラスの初期化で作成されます。これは、後で使用サイトで
    • 複数のスレッドから同時に呼び出される
    • 最初の呼び出しのパフォーマンスの低下に苦しむ
于 2014-06-02T09:30:48.790 に答える
9

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

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

実行時のメソッド参照式の評価は、通常の完了によってオブジェクトへの参照が生成される限り、クラス インスタンス作成式の評価と同様です。[..]

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

簡単なテストは、静的メソッドのメソッド参照が各評価で同じ参照になる (可能性がある) ことを示しています。次のプログラムは 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);
    }
}

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

したがって、この手動による最適化の価値を判断するためのパフォーマンス分析がない限り、私はそれを行わないことを強くお勧めします。キャッシングはコードの可読性に影響を与え、価値があるかどうかは不明です。時期尚早の最適化は諸悪の根源です。

于 2014-06-01T21:10:39.810 に答える