例えば:
a)int [x][y][z]
vs
b)int[x*y*z]
当初は、簡単にするためにa)を使用すると思いました。
JavaはCのように配列をメモリに線形に格納しないことは知っていますが、これは私のプログラムにどのような影響を及ぼしますか?
通常、このような質問の回答を検索するときに行う最善のことは、選択肢がどのようにJVMバイトコードにコンパイルされるかを確認することです。
_multi = new int[50][50];
single = new int[2500];
_
これは次のように翻訳されます。
_BIPUSH 50
BIPUSH 50
MULTIANEWARRAY int[][] 2
ASTORE 1
SIPUSH 2500
NEWARRAY T_INT
ASTORE 2
_
したがって、ご覧のとおり、JVMは、多次元配列について話していることをすでに認識しています。
それをさらに保つ:
_for (int i = 0; i < 50; ++i)
for (int j = 0; j < 50; ++j)
{
multi[i][j] = 20;
single[i*50+j] = 20;
}
_
これは(サイクルをスキップして)次のように変換されます。
_ALOAD 1: multi
ILOAD 3: i
AALOAD
ILOAD 4: j
BIPUSH 20
IASTORE
ALOAD 2: single
ILOAD 3: i
BIPUSH 50
IMUL
ILOAD 4: j
IADD
BIPUSH 20
IASTORE
_
したがって、ご覧のとおり、多次元配列はVMの内部で処理され、無駄な命令によってオーバーヘッドが生成されることはありませんが、オフセットは手動で計算されるため、単一の配列を使用するとより多くの命令が使用されます。
パフォーマンスがそんなに問題になるとは思いません。
編集:
ここで何が起こっているかを確認するために、いくつかの簡単なベンチマークを実行しました。線形読み取り、線形書き込み、ランダムアクセスなどのさまざまな例を試すことにしました。時間はミリ秒で表されます(そしてSystem.nanoTime()
を使用して計算されます。結果は次のとおりです。
線形書き込み
線形読み取り
ランダム読み取り
ランダムなものは、多次元配列用に2つの乱数を生成し、1次元用に1つだけを生成するため、少し誤解を招く可能性があります(PNRGはCPUを消費する可能性があります)。
同じループを20回実行した後でのみ、ベンチマークを実行してJITを機能させようとしたことに注意してください。完全を期すために、私のJava VMは次のとおりです。
Javaバージョン "1.6.0_17" Java(TM)SEランタイム環境(ビルド1.6.0_17-b04)Java HotSpot(TM)64ビットサーバーVM (ビルド14.3-b01、混合モード)
現在のCPUでは、キャッシュされていないメモリアクセスは、算術演算よりも数百倍遅くなります( このプレゼンテーション および読み取り すべてのプログラマーがメモリについて知っておくべきこと を参照)。 a)オプションでは約3回のメモリルックアップが発生しますが、b)オプションでは約1回のメモリルックアップが発生します。また、CPUのプリフェッチアルゴリズムも機能しない可能性があります。したがって、b)オプションは、状況によってはより高速になる可能性があります(これはホットスポットであり、アレイがCPUのキャッシュに収まりません)。どれくらい速いですか? -それはアプリケーションによって異なります。
個人的には、最初にa)オプションを使用します。これは、コードが単純になるためです。プロファイラーが配列アクセスがボトルネックであることを示した場合、それをb)オプションに変換します。これにより、配列値を読み書きするための2つのヘルパーメソッドがあります(これにより、厄介なコードはこれら2つに制限されます。メソッド)。
3次元のint配列(「Multi」列)を同等の1次元のint配列(「Single」列)と比較するためのベンチマークを作成しました。コードは ここ であり、テスト ここ です。私はそれを64ビットjdk1.6.0_18、Windows 7 x64、Core 2 Quad Q6600 @ 3.0 GHz、4 GB DDR2で、JVMオプション_-server -Xmx3G -verbose:gc -XX:+PrintCompilation
_を使用して実行しました(次の結果からデバッグ出力を削除しました)。結果は次のとおりです。
_Out of 20 repeats, the minimum time in milliseconds is reported.
Array dimensions: 100x100x100 (1000000)
Multi Single
Seq Write 1 1
Seq Read 1 1
Random Read 99 90 (of which generating random numbers 59 ms)
Array dimensions: 200x200x200 (8000000)
Multi Single
Seq Write 14 13
Seq Read 11 8
Random Read 1482 1239 (of which generating random numbers 474 ms)
Array dimensions: 300x300x300 (27000000)
Multi Single
Seq Write 53 46
Seq Read 34 24
Random Read 5915 4418 (of which generating random numbers 1557 ms)
Array dimensions: 400x400x400 (64000000)
Multi Single
Seq Write 123 111
Seq Read 71 55
Random Read 16326 11144 (of which generating random numbers 3693 ms)
_
これは、1次元配列が高速であることを示しています。違いは非常に小さいですが、99%のアプリケーションでは目立たないでしょう。
また、preventOptimizingAway += array.get(x, y, z);
を_preventOptimizingAway += x * y * z;
_に置き換えて、ランダム読み取りベンチマークで乱数を生成するオーバーヘッドを推定するためにいくつかの測定を行い、測定値を上記の結果テーブルに手動で追加しました。乱数の生成には、ランダム読み取りベンチマークの合計時間の3分の1以下しかかからないため、予想どおり、メモリアクセスがベンチマークを支配します。 4次元以上の配列でこのベンチマークを繰り返すのは興味深いことです。多次元配列の最上位レベルがCPUのキャッシュに収まり、他のレベルのみがメモリルックアップを必要とするため、おそらく速度差が大きくなります。
最初のバリアント(3次元)を使用すると、理解しやすく、論理エラーが発生する可能性が低くなります(特に、3次元空間のモデリングに使用している場合)。
後者のルートを選択した場合は、単一のアレイアクセスごとに演算を実行する必要があります。これは、(この機能を提供するクラスでラップしない限り)苦痛とエラーが発生しやすくなります。
フラット配列を選択する際に(重要な)最適化があるとは思いません(特に、それにインデックスを付けるために必要な算術を考えると)。最適化の場合と同様に、いくつかの測定を実行して、それが本当に価値があるかどうかを判断する必要があります。