65要素の配列を宣言するよりも1000倍速く64要素の複数の配列を宣言する


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 + "";

なぜこれが事実であり、この問題を回避することが可能であるかと思っていました。


3
オフノート:ベンチマークよりSystem.nanoTime()も推奨さSystem.currentTimeMillis()れます。
ロケットボーイ

4
ただ興味があるだけ ?Linuxを使用していますか?OSによって動作は変わりますか?
bsd 2013

9
この質問は一体どのようにして反対票を得ましたか?
Rohit Jain 2013

2
FWIW、のbyte代わりにでこのコードを実行すると、同様のパフォーマンスの違いが見られdoubleます。
Oliver Charlesworth 2013

3
@ThomasJungblut:では、OPの実験の矛盾を説明するものは何ですか?
Oliver Charlesworth 2013

回答:


88

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

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

double[] test = new double[64];

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

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

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

それでは詳細に進みましょう。当然のことながら、64要素以下のスカラー配列に対してトリガーされる最適化があります。最適化は、エスケープ分析の一部です。小さなオブジェクトと小さな配列をヒープに割り当てるのではなく、スタックに配置します。あるいは、それらを完全に最適化して削除します。これに関するいくつかの情報は、2005年に書かれたBrian Goetzによる次の記事にあります。

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

java -XX:EliminateAllocationArraySizeLimit=65 Tests

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


9
しかし、なぜオプティマイザはサイズ64のアレイが取り外し可能であるが65ではないことを検出しているのですか
ug_

10
@nosid:OPのコードは現実的ではないかもしれませんが、JVMで興味深い/予期しない動作を明確に引き起こしているため、他の状況に影響を与える可能性があります。なぜこれが起こっているのかを尋ねることは妥当だと思います。
Oliver Charlesworth 2013

1
@ThomasJungblutループが削除されるとは思いません。ループの外側に「int total」を追加し、「total + = test [0];」を追加できます。上記の例に。次に、結果を出力すると、合計= 1億で、1秒未満で実行されます。
Sipko 2013

1
オンスタック置換は、ヒープ割り当てをスタック割り当てで置き換える代わりに、解釈済みコードをオンザフライでコンパイルしたものに置き換えることです。EliminateAllocationArraySizeLimitは、エスケープ分析でスカラー置換可能と見なされる配列の制限サイズです。したがって、効果がコンパイラの最適化によるものであるという主要な点は正しいですが、スタックの割り当てによるものではなく、エスケープ分析フェーズが割り当ての必要性に気付かないためです。
キヘル2013

2
@Sipko:アプリケーションがスレッドの数に応じてスケーリングしないことを書いています。それは問題があなたが求めているマイクロ最適化に関連していないことを示しています。細かい部分ではなく、全体像を見ることをお勧めします。
nosid 2013

2

オブジェクトのサイズに基づいて、差異が生じる可能性のある方法はいくつもあります。

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

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

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

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


このスレッドに遅れます。スタックへの割り当てがヒープへの割り当てよりも速いのはなぜですか?いくつかの記事によると、ヒープへの割り当てには約12命令が必要です。改善の余地はあまりありません。
Vortex

@Vortex-スタックへの割り当てには1〜2命令かかります。しかし、それはスタックフレーム全体を割り当てることです。スタックフレームは、ルーチンのレジスター保存領域を確保するためにとにかく割り当てる必要があるため、同時に割り当てられる他の変数はすべて「フリー」です。そして、私が言ったように、スタックはGCを必要としません。ヒープアイテムのGCオーバーヘッドは、ヒープ割り当て操作のコストよりもはるかに大きくなります。
ホットリックス2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.