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

コードのホットな位置に表示されるメソッド参照をキャッシュする必要がありますか、それともVMがこれを最適化して、キャッシュを不要にすることができますか?これに関する一般的なベストプラクティスはありますか、それともこのようなキャッシュが役立つかどうかにかかわらず、この高度にVMの実装に固有ですか?


このレベルのチューニングが必要/望ましいほどその関数を使用していると思います。ラムダを削除して関数を直接実装すると、他の最適化の余地が増える可能性があります。
SJuan76 2014年

@ SJuan76:これについてはよくわかりません!メソッド参照が匿名クラスにコンパイルされている場合、通常のインターフェイス呼び出しと同じくらい高速です。したがって、ホットコードでは機能的なスタイルを避けるべきではないと思います。
gexicide 2014年

4
メソッド参照はで実装されinvokedynamicます。関数オブジェクトをキャッシュすることでパフォーマンスが向上するかどうかは疑問です。それどころか、コンパイラの最適化を妨げる可能性があります。2つのバリアントのパフォーマンスを比較しましたか?
nosid 2014年

@nosid:いいえ、比較はしませんでした。しかし、私はOpenJDKの非常に初期のバージョンを使用しているので、最初のバージョンは新しい機能をすばやく「n」ダーティで実装するだけであり、機能が成熟したときのパフォーマンスと比較できないと思うので、私の数字はとにかく重要ではないかもしれません時間とともに。仕様は本当にinvokedynamic使用しなければならないことを義務付けていますか?ここに理由はわかりません!
gexicide 2014年

4
自動的にキャッシュされるはずなので(毎回新しい匿名クラスを作成するのと同じではありません)、その最適化について心配する必要はありません。
assylias 2014年

回答:


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

ここでは、同じ呼び出しサイトが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に任されてinvokedynamicCallSiteますが、最初の呼び出しで作成されたインスタンスを記憶して再利用するのは、命令の指定された動作です。

現在のJREは、ステートレスラムダの定数オブジェクトをConstantCallSite含むを生成しMethodHandleます(これを別の方法で行う理由は考えられません)。また、メソッドへのメソッド参照staticは常にステートレスです。したがって、ステートレスラムダとシングルコールサイトの場合、答えは次のようになります。キャッシュしないでください。JVMはキャッシュします。キャッシュしない場合は、打ち消すべきではないという強い理由が必要です。

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

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

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


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

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

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

5
明確化:「call-site」という用語invokedynamicは、ラムダを作成する命令の実行を指します。関数型インターフェースメソッドが実行される場所ではありません。
ホルガー2014年

1
this-capturing lambdasは、インスタンススコープのシングルトン(オブジェクト上の合成インスタンス変数)だと思いました。これはそうではありませんか?
Marko Topolnik 2014

2
@Marko Topolnik:これは準拠したコンパイル戦略ですが、Oracleのjdkの時点で1.8.0_40はそうではありません。これらのラムダは記憶されないため、ガベージコレクションが可能です。ただし、invokedynamicコールサイトがリンクされると、通常のコードのように最適化される可能性があることに注意してください。つまり、エスケープ分析はそのようなラムダインスタンスに対して機能します。
ホルガー2014

2
という名前の標準ライブラリクラスはないようですMethodReference。あなたが意味するかMethodHandleここ?
lii 2015

2
@Lii:その通りです、それはタイプミスです。興味深いことに、これまで誰も気づかなかったようです。
ホルガー

11

残念ながら、それが良い理想である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を必要としないのは良かったでしょう。


ああ、要点がわかりました。OPが話しているシナリオではないかもしれませんが、合理的に聞こえます。それにもかかわらず、賛成した。
Tagir Valeev 2015

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.