91

最近、64 要素を含む配列を宣言する方が、65 要素を含む同じタイプの配列を宣言するよりもはるかに高速 (>1000 倍) であることに気付きました。

これをテストするために使用したコードは次のとおりです。

public class Tests{
    public static void main(String args[]){
        double start = System.nanoTime();
        int job = 100000000;//100 million
        for(int i = 0; i < job; i++){
            double[] test = new double[64];
        }
        double end = System.nanoTime();
        System.out.println("Total runtime = " + (end-start)/1000000 + " ms");
    }
}

これは約 6 ミリ秒で実行されます。置き換えるnew double[64]new double[65]、約 7 秒かかります。ジョブがますます多くのスレッドに分散されると、この問題は指数関数的に深刻になります。これが私の問題の原因です。

int[65]この問題は、やなどのさまざまなタイプの配列でも発生しますString[65]。この問題は大きな文字列:String test = "many characters";では発生しませんが、これを次のように変更すると発生し始めます。String test = i + "";

なぜそうなるのか、この問題を回避できるのかどうか疑問に思っていました。

4

2 に答える 2

88

Java VM の JIT コンパイラーによって行われた最適化によって引き起こされる動作を観察しています。この動作は、最大 64 要素のスカラー配列でトリガーされて再現可能であり、64 を超える配列ではトリガーされません。

詳細に入る前に、ループの本体を詳しく見てみましょう。

double[] test = new double[64];

体には何の影響もありません(観察可能な行動) . つまり、このステートメントが実行されるかどうかにかかわらず、プログラムの実行以外では違いはありません。同じことがループ全体にも当てはまります。そのため、コード オプティマイザーがループを同じ機能的で異なるタイミング動作を持つ何か(または何もないもの)に変換することが発生する可能性があります。

ベンチマークについては、少なくとも次の 2 つのガイドラインに従う必要があります。そうすれば、差はかなり小さくなります。

  • ベンチマークを数回実行して、JIT コンパイラ (およびオプティマイザ) をウォームアップします。
  • すべての式の結果を使用して、ベンチマークの最後に出力します。

それでは詳細を見ていきましょう。驚くことではありませんが、64 要素以下のスカラー配列に対してトリガーされる最適化があります。最適化はEscape 分析の一部です。小さなオブジェクトと小さな配列をヒープに割り当てる代わりに、スタックに配置します。または、それらを完全に最適化することもできます。これについては、2005 年に Brian Goetz が書いた次の記事を参照してください。

コマンドラインオプションで最適化を無効にすることができます-XX:-DoEscapeAnalysis。スカラー配列のマジック値 64 も、コマンド ラインで変更できます。次のようにプログラムを実行すると、要素数が 64 の配列と 65 の配列に違いはありません。

java -XX:EliminateAllocationArraySizeLimit=65 Tests

そうは言っても、そのようなコマンドラインオプションを使用することは強くお勧めしません. 実際のアプリケーションで大きな違いが生じるとは思えません。必要性を完全に確信している場合にのみ使用し、いくつかの疑似ベンチマークの結果に基づいていません。

于 2013-09-15T09:20:33.070 に答える
2

オブジェクトのサイズに基づいて、さまざまな方法で違いが生じる可能性があります。

nosid が述べたように、JITC はスタックに小さな「ローカル」オブジェクトを割り当てている可能性があり (ほとんどの場合そうです)、「小さな」配列のサイズ カットオフは 64 要素である可能性があります。

スタックへの割り当ては、ヒープでの割り当てよりもはるかに高速です。さらに重要なのは、スタックをガベージ コレクションする必要がないため、GC オーバーヘッドが大幅に削減されます。(そして、このテスト ケースでは、GC オーバーヘッドは合計実行時間の 80 ~ 90% になる可能性があります。)

さらに、値がスタックに割り当てられると、JITC は「デッド コードの削除」を実行し、その結果がnewどこにも使用されないことを確認し、失われる副作用がないことを確認した後、new操作全体を削除します。次に、(現在は空の) ループ自体です。

JITC がスタック割り当てを行わない場合でも、特定のサイズよりも小さいオブジェクトが、大きなオブジェクトとは異なる方法で (たとえば、別の「スペース」から) ヒープに割り当てられる可能性は十分にあります。(ただし、通常、これによってそれほど劇的なタイミングの違いが生じることはありません。)

于 2013-09-15T13:29:55.960 に答える