web-dev-qa-db-ja.com

Java:多次元配列と1次元

例えば:

  • a)int [x][y][z]

    vs

  • b)int[x*y*z]

当初は、簡単にするためにa)を使用すると思いました。

JavaはCのように配列をメモリに線形に格納しないことは知っていますが、これは私のプログラムにどのような影響を及ぼしますか?

28
Mikolan

通常、このような質問の回答を検索するときに行う最善のことは、選択肢がどのように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()を使用して計算されます。結果は次のとおりです。

線形書き込み

  • サイズ:100x100(10000)マルチ:5.786591シングル:6.131748
  • サイズ:200x200(40000)マルチ:1.216366シングル:0.782041
  • サイズ:500x500(250000)マルチ:7.177029シングル:3.667017
  • サイズ:1000x1000(1000000)マルチ:30.508131シングル:18.064592
  • サイズ:2000x2000(4000000)マルチ:185.3548シングル:155.590313
  • サイズ:5000x5000(25000000)マルチ:955.5299シングル:923.264417
  • サイズ:10000x10000(100000000)マルチ:4084.798753シングル:4015.448829

線形読み取り

  • サイズ:100x100(10000)マルチ:5.241338シングル:5.135957
  • サイズ:200x200(40000)マルチ:0.080209シングル:0.044371
  • サイズ:500x500(250000)マルチ:0.088742シングル:0.084476
  • サイズ:1000x1000(1000000)マルチ:0.232095シングル:0.167671
  • サイズ:2000x2000(4000000)マルチ:0.481683シングル:0.33321
  • サイズ:5000x5000(25000000)マルチ:1.222339シングル:0.828118サイズ:10000x10000(100000000)マルチ:2.496302シングル:1.650691

ランダム読み取り

  • サイズ:100x100(10000)マルチ:22.317393シングル:8.546134
  • サイズ:200x200(40000)マルチ:32.287669シングル:11.022383
  • サイズ:500x500(250000)マルチ:189.542751シングル:68.181343
  • サイズ:1000x1000(1000000)マルチ:1124.78609シングル:272.235584
  • サイズ:2000x2000(4000000)マルチ:6814.477101シングル:1091.998395
  • サイズ:5000x5000(25000000)マルチ:50051.306239シングル:7028.422262

ランダムなものは、多次元配列用に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、混合モード)

67
Jack

現在の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のキャッシュに収まり、他のレベルのみがメモリルックアップを必要とするため、おそらく速度差が大きくなります。

22
Esko Luontola

最初のバリアント(3次元)を使用すると、理解しやすく、論理エラーが発生する可能性が低くなります(特に、3次元空間のモデリングに使用している場合)。

4
Roman

後者のルートを選択した場合は、単一のアレイアクセスごとに演算を実行する必要があります。これは、(この機能を提供するクラスでラップしない限り)苦痛とエラーが発生しやすくなります。

フラット配列を選択する際に(重要な)最適化があるとは思いません(特に、それにインデックスを付けるために必要な算術を考えると)。最適化の場合と同様に、いくつかの測定を実行して、それが本当に価値があるかどうかを判断する必要があります。

2
Brian Agnew