2011-07-06 4 views
14

私はいくつかのJavaプリミティブコレクション(trovefastutilhppc)を見てきましたが、クラス変数がfinalローカル変数として宣言されるパターンに気付きました。たとえば:Javaのクラス変数よりも最終ローカル変数にアクセスする方が速いのですか?

public void forEach(IntIntProcedure p) { 
    final boolean[] used = this.used; 
    final int[] key = this.key; 
    final int[] value = this.value; 
    for (int i = 0; i < used.length; i++) { 
     if (used[i]) { 
      p.apply(key[i],value[i]); 
     } 
    } 
} 

私はいくつかのベンチマークを行ってきた、そしてそれは、これを行うとき、それは少し高速であることが表示されますが、なぜこれがそうですか?関数の最初の3行がコメントアウトされていれば、Javaが何をするのかを理解しようとしています。

注:これはthis questionと似ていますが、これはC++用であり、なぜfinalと宣言されたのかについては言及していません。

+1

あなたは、違いを確認するために生成されたJavaアセンブリを調べることができます。 –

+0

ちょうど理性がHotSpotコンパイラでバイトコード自体ではないことに気づいた... –

+0

ベンチマークコードを投稿してください。少なくともベンチマークの方法が間違っていて、コンパイラではなくインタープリタだけをテストしてください:) – Voo

答えて

8

finalキーワードはここでは赤い鳴きです。 パフォーマンスの違いは、2つの異なることを言っているからです。

public void forEach(IntIntProcedure p) { 
    final boolean[] used = this.used; 
    for (int i = 0; i < used.length; i++) { 
    ... 
    } 
} 

は、「ブール配列を取得し、 その配列の各要素のために何かをする。」、と言っています final boolean[] usedなし

は、この関数は、インデックスが現在のオブジェクトのusedフィールドの現在値の長さよりも小さいながら、現在のオブジェクトのusedフィールドの現在値を取得して何かをする」と言っていますインデックスiの要素。

usedの値を変更する原因となるものをはるかに簡単に判断できるため、JITではループバインド不変条件を使用して過剰なバインドチェックなどを排除することができます。 p.applyusedの値を変更する可能性がある場合は、複数のスレッドを無視しても、JITは境界チェックを排除したり、他の有用な最適化を行うことはできません。

+0

私は、あなたが「最終」という言葉が赤いニシンであると混乱しています。変数にアクセスする必要は必ずしも速いわけではありませんが、JITコンパイラはループを最適化して範囲チェックと参照を排除できますか? – job

+0

「複数のスレッドを無視する」 - これを明確にするには:JIT **のみ**はスレッドのローカル動作を考慮します。これは、たとえ使用されていてもpublic(またはsetterメソッドがある)であって、別のスレッドによって変更されても、JITはこれを無視することができます。したがって、JITは実際にapply()が参照を変更するかどうかを判断するだけです(実際には呼び出しをインライン化することができればそれに気づくでしょう)。そうでなければ、 – Voo

+0

また、誰かが無効なJavaベンチマークをもう一度書いてしまったため、速い動作が起こる可能性があります。非常にシンプルですが、実際には現代のHotspot – Voo

2

実行時(jit)は、そのメソッド呼び出しの文脈で、これらの3つの値は決して変更されないため、ランタイムはメンバ変数から値を継続的にロードする必要はないと伝えます。これはわずかなスピードの向上をもたらすかもしれない。

もちろん、ジットがよりスマートになり、これらのことを単独で把握できるようになると、これらの規則はあまり役に立たなくなります。

メモ、私はスピードアップが最終的な部分よりもローカル変数を使用していることを明らかにしていませんでした。

+0

ねえ、私もこれをタイプしていました! :-)私は、コンパイラでさえ、メソッドがこれらの参照の並列変更には興味がないことを知ることで利益を得ることができると考えている以外は、 – Szocske

25

ローカル変数またはパラメータへのアクセスは、単一ステップの操作です。スタック上のオフセットNに位置する変数を取ります。 this

  • N = 1 - - 第一引数
  • N = 2 - 第二引数
  • N = 3 - 最初のローカル変数
    • N = 0:あなたが機能する場合は2つの引数(簡略化)を有します
    • N = 4秒のローカル変数
    • ...

    ローカル変数にアクセスすると、固定オフセットで1つのメモリアクセスがあります(Nはコンパイル時にわかります)。

    iload 1 //N = 1 
    

    あなたがフィールドにアクセスするときしかし、あなたが実際に余分なステップを実行している:これは最初のメソッドの引数(int)にアクセスするためのバイトコードです。最初に、現在のオブジェクトアドレスを決定するために、 "ローカル変数" thisを読み込んでいます。次に、thisから固定オフセットを持つフィールド(getfield)をロードしています。つまり、1つ(または1つの余分)ではなく2つのメモリ操作を実行します。バイトコード:

    aload 0 //N = 0: this reference 
    getfield total I //int total 
    

    技術的には、ローカル変数とパラメータにアクセスするのがオブジェクトフィールドよりも高速です。実際には、他の多くの要因がパフォーマンス(CPUキャッシュとJVMのさまざまなレベルの最適化を含む)に影響する可能性があります。

    finalは別の話です。基本的にコンパイラ/ JITのヒントです。このリファレンスは変更されないため、より重い最適化を行うことができます。しかし、これは追跡するのがずっと難しく、可能であれば、経験則としてfinalを使用してください。

    +5

    私はこの回答(と特に最後のパラグラフ)がマークされたものより良いと思います。 –

    +0

    最終的なスピードアップの中には、スマートなJITがオブジェクトがスコープから外れる前にポインタを再利用することがわかっていて、alloc()を保存して、メモリの容量を少し少なくするフットプリント... – Ajax

    +0

    完全に同意します。最も有用な答え。 – omniyo

    1

    生成されたVMオペコードでは、ローカル変数はオペランドスタックのエントリであり、フィールド参照はオブジェクト参照を介して値を取得する命令によってスタックに移動する必要があります。私は、JITがスタック参照が参照をより簡単に参照できるようにすることができると思います。

    +2

    正しくはありません。ローカル変数は、* operand stack *ではなくthread * stack *に配置されます。種々の「ロード」/「ストア」オペコードが、ローカル変数をスタックからオペランドスタックに戻し戻すために使用される。 [このイメージ](http://www.ibm.com/developerworks/ibm/library/it-haggar_bytecode/fig01.gif)を参照してください。 –

    0

    このような単純な最適化は、すでにJVMランタイムに組み込まれています。 JVMがインスタンス変数への単純なアクセスを行うと、私たちのJavaアプリケーションは非常に遅くなります。

    このような手動調整は、より単純なJVMの場合にはおそらく価値があります。アンドロイド。

    +0

    デックス(アンドロイド)バイトコードはおそらくもっと効率的です...圧縮されていない.dxはjar圧縮された.classよりも小さく、javaモバイルよりもdalvikの全体的な理由はパフォーマンスです(標準のjvmはモバイルデバイスにとってはあまりにも膨らんでいます) – Ajax

    関連する問題