web-dev-qa-db-ja.com

多次元配列とC#の配列の配列の違いは何ですか?

C#の多次元配列double[,]と配列配列double[][]の違いは何ですか?

違いがある場合、それぞれのための最良の用途は何ですか?

422
ecleel

配列の配列(ギザギザ配列)は多次元配列よりも高速であり、より効果的に使用できます。多次元配列はより良い構文を持っています。

ギザギザ配列と多次元配列を使用して単純なコードを作成し、IL逆アセンブラを使用してコンパイル済みのAssemblyを調べると、多次元配列に対する同じ操作はメソッドでありながら、単純なIL命令であることがわかります。常に遅い呼び出し。

以下の方法を検討してください。

static void SetElementAt(int[][] array, int i, int j, int value)
{
    array[i][j] = value;
}

static void SetElementAt(int[,] array, int i, int j, int value)
{
    array[i, j] = value;
}

彼らのILは次のようになります。

.method private hidebysig static void  SetElementAt(int32[][] 'array',
                                                    int32 i,
                                                    int32 j,
                                                    int32 'value') cil managed
{
  // Code size       7 (0x7)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  ldelem.ref
  IL_0003:  ldarg.2
  IL_0004:  ldarg.3
  IL_0005:  stelem.i4
  IL_0006:  ret
} // end of method Program::SetElementAt

.method private hidebysig static void  SetElementAt(int32[0...,0...] 'array',
                                                    int32 i,
                                                    int32 j,
                                                    int32 'value') cil managed
{
  // Code size       10 (0xa)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  ldarg.2
  IL_0003:  ldarg.3
  IL_0004:  call       instance void int32[0...,0...]::Set(int32,
                                                           int32,
                                                           int32)
  IL_0009:  ret
} // end of method Program::SetElementAt

ギザギザ配列を使用すると、行スワップや行サイズ変更などの操作を簡単に実行できます。多次元配列を使用する方が安全な場合もあるかもしれませんが、Microsoft FxCopでも、プロジェクトを分析するときに多次元配列の代わりにギザギザ配列を使用することをお勧めします。

313
okutane

多次元配列はNice線形メモリレイアウトを作成しますが、ギザギザ配列はいくつかの追加レベルの間接参照を意味します。

ギザギザ配列で値jagged[3][6]を調べるvar jagged = new int[10][5]は、次のように機能します。インデックス3の要素(配列)を調べ、その配列のインデックス6の要素(値)を調べます。この場合の各次元について、追加の検索があります(これは高価なメモリアクセスパターンです)。

多次元配列はメモリに線形に配置され、実際の値はインデックスを掛け合わせることによって求められます。ただし、配列var mult = new int[10,30]を指定すると、その多次元配列のLengthプロパティは要素の総数、つまり10 * 30 = 300を返します。

ギザギザ配列のRankプロパティは常に1ですが、多次元配列は任意のランクを持つことができます。各次元の長さを取得するには、任意の配列のGetLengthメソッドを使用できます。この例の多次元配列の場合、mult.GetLength(1)は30を返します。

多次元配列の索引付けは高速です。例えばこの例の多次元配列mult[1,7] = 30 * 1 + 7 = 37を指定して、そのインデックス37の要素を取得します。配列のベースアドレスである1つのメモリ位置のみが含まれるため、これはより良いメモリアクセスパターンです。

したがって、多次元配列は連続したメモリブロックを割り当てますが、ギザギザ配列は正方形である必要はありません。 jagged[1].Lengthjagged[2].Lengthと同じである必要はありません。これはどの多次元配列にも当てはまります。

パフォーマンス

パフォーマンス面では、多次元配列のほうが速いはずです。もっと速いですが、本当に悪いCLR実装のためにそうではありません。

 23.084  16.634  15.215  15.489  14.407  13.691  14.695  14.398  14.551  14.252 
 25.782  27.484  25.711  20.844  19.607  20.349  25.861  26.214  19.677  20.171 
  5.050   5.085   6.412   5.225   5.100   5.751   6.650   5.222   6.770   5.305 

1行目はギザギザ配列のタイミング、2行目は多次元配列、3行目はそうであるべきです。プログラムは以下の通りです、FYIこれはモノラルで動いてテストされました。 (ウィンドウのタイミングは、主にCLRの実装の違いによって大きく異なります)。

Windowsでは、ギザギザ配列のタイミングは非常に優れています。多次元配列のルックアップがどのようなものであるべきかについての私自身の解釈とほぼ同じです。 'Single()'を参照してください。残念なことにwindows JITコンパイラは本当にばかげています、そしてこれは残念ながらこれらのパフォーマンスの議論を困難にします、あまりにも多くの矛盾があります。

これは私がWindowsで得たタイミングです。ここでも同じです。最初の行はギザギザ配列、2番目は多次元、3番目は私自身の多次元の実装です。

  8.438   2.004   8.439   4.362   4.936   4.533   4.751   4.776   4.635   5.864
  7.414  13.196  11.940  11.832  11.675  11.811  11.812  12.964  11.885  11.751
 11.355  10.788  10.527  10.541  10.745  10.723  10.651  10.930  10.639  10.595

ソースコード:

using System;
using System.Diagnostics;
static class ArrayPref
{
    const string Format = "{0,7:0.000} ";
    static void Main()
    {
        Jagged();
        Multi();
        Single();
    }

    static void Jagged()
    {
        const int dim = 100;
        for(var passes = 0; passes < 10; passes++)
        {
            var timer = new Stopwatch();
            timer.Start();
            var jagged = new int[dim][][];
            for(var i = 0; i < dim; i++)
            {
                jagged[i] = new int[dim][];
                for(var j = 0; j < dim; j++)
                {
                    jagged[i][j] = new int[dim];
                    for(var k = 0; k < dim; k++)
                    {
                        jagged[i][j][k] = i * j * k;
                    }
                }
            }
            timer.Stop();
            Console.Write(Format,
                (double)timer.ElapsedTicks/TimeSpan.TicksPerMillisecond);
        }
        Console.WriteLine();
    }

    static void Multi()
    {
        const int dim = 100;
        for(var passes = 0; passes < 10; passes++)
        {
            var timer = new Stopwatch();
            timer.Start();
            var multi = new int[dim,dim,dim];
            for(var i = 0; i < dim; i++)
            {
                for(var j = 0; j < dim; j++)
                {
                    for(var k = 0; k < dim; k++)
                    {
                        multi[i,j,k] = i * j * k;
                    }
                }
            }
            timer.Stop();
            Console.Write(Format,
                (double)timer.ElapsedTicks/TimeSpan.TicksPerMillisecond);
        }
        Console.WriteLine();
    }

    static void Single()
    {
        const int dim = 100;
        for(var passes = 0; passes < 10; passes++)
        {
            var timer = new Stopwatch();
            timer.Start();
            var single = new int[dim*dim*dim];
            for(var i = 0; i < dim; i++)
            {
                for(var j = 0; j < dim; j++)
                {
                    for(var k = 0; k < dim; k++)
                    {
                        single[i*dim*dim+j*dim+k] = i * j * k;
                    }
                }
            }
            timer.Stop();
            Console.Write(Format,
                (double)timer.ElapsedTicks/TimeSpan.TicksPerMillisecond);
        }
        Console.WriteLine();
    }
}
189
John Leidegren

簡単に言うと、多次元配列はDBMSのテーブルに似ています。
Array of Array(ギザギザ配列)を使用すると、各要素に同じタイプの可変長の別の配列を保持させることができます。

そのため、データ構造がテーブル(固定の行/列)のように見えることが確実な場合は、多次元配列を使用できます。ギザギザ配列は固定要素であり、各要素は可変長の配列を保持できます

例えば。疑似コード:

int[,] data = new int[2,2];
data[0,0] = 1;
data[0,1] = 2;
data[1,0] = 3;
data[1,1] = 4;

上記を2×2の表と考えてください。

1 | 2
3 | 4
int[][] jagged = new int[3][]; 
jagged[0] = new int[4] {  1,  2,  3,  4 }; 
jagged[1] = new int[2] { 11, 12 }; 
jagged[2] = new int[3] { 21, 22, 23 }; 

上記のことを各行が可変数の列を持つと考えてください。

 1 |  2 |  3 | 4
11 | 12
21 | 22 | 23
65
shahkalpesh

序文:このコメントは okutaneによる回答 を対象としていますが、SOの愚かな評判システムのため、私はできません属する場所に投稿してください。

メソッド呼び出しのために一方が他方より遅いというあなたの主張は正しくありません。より複雑な境界検査アルゴリズムのために、一方が他方より遅くなります。 ILではなく、コンパイルされたアセンブリを見ることでこれを簡単に確認できます。たとえば、私の4.5インストールでは、eaxとedxに格納されたインデックスを使用してecxが指す2次元配列に格納された要素に(edxのポインタ経由で)アクセスすると、次のようになります。

sub eax,[ecx+10]
cmp eax,[ecx+08]
jae oops //jump to throw out of bounds exception
sub edx,[ecx+14]
cmp edx,[ecx+0C]
jae oops //jump to throw out of bounds exception
imul eax,[ecx+0C]
add eax,edx
lea edx,[ecx+eax*4+18]

ここでは、メソッド呼び出しによるオーバーヘッドがないことがわかります。境界チェックは、ゼロ以外のインデックスの可能性があるため、非常に複雑です。これは、ギザギザ配列では提供されていない機能です。 0以外の場合のsub、cmp、およびjmpsを削除すると、コードはほとんど(x*y_max+y)*sizeof(ptr)+sizeof(array_header)に解決されます。この計算は、要素へのランダムアクセスの場合と同じくらい高速です(1乗算をシフトで置き換えることができます。2ビットのべき乗としてサイズ設定するバイトを選択するのはこれが理由です)。

もう1つの複雑な点は、現代のコンパイラが、1次元配列を反復処理しながら、要素アクセスのネストされた境界チェックを最適化することがたくさんあるということです。その結果、基本的に配列の連続したメモリ上でインデックスポインタを進めるだけのコードになります。多次元配列に対する単純反復は一般に、追加のネストロジック層を含むため、コンパイラは操作を最適化する可能性が低くなります。そのため、1つの要素にアクセスすることによる境界チェックのオーバーヘッドが、配列のサイズとサイズに関して一定の実行時間で償却されても、違いを測定するための単純なテストケースの実行には何倍も時間がかかります。

38
Eglin

。NET Core多次元配列がギザギザ配列より速いので、これについて更新したいと思います。 John Leidegren からテストを実行しました。これらは.NET Core 2.0プレビュー2の結果です。バックグラウンドアプリケーションからの影響を見えにくくするために、ディメンションの値を大きくしました。

Debug (code optimalization disabled)
Running jagged 
187.232 200.585 219.927 227.765 225.334 222.745 224.036 222.396 219.912 222.737 

Running multi-dimensional  
130.732 151.398 131.763 129.740 129.572 159.948 145.464 131.930 133.117 129.342 

Running single-dimensional  
 91.153 145.657 111.974  96.436 100.015  97.640  94.581 139.658 108.326  92.931 


Release (code optimalization enabled)
Running jagged 
108.503 95.409 128.187 121.877 119.295 118.201 102.321 116.393 125.499 116.459 

Running multi-dimensional 
 62.292  60.627  60.611  60.883  61.167  60.923  62.083  60.932  61.444  62.974 

Running single-dimensional 
 34.974  33.901  34.088  34.659  34.064  34.735  34.919  34.694  35.006  34.796 

私は分解を調べました、そして、これは私が見つけたものです

jagged[i][j][k] = i * j * k;を実行するには34個の命令が必要です

multi[i, j, k] = i * j * k;を実行するには11の命令が必要です

single[i * dim * dim + j * dim + k] = i * j * k;を実行するには23の命令が必要です

なぜ一次元配列が多次元配列よりも速いのかを特定することはできませんでしたが、それはCPUの最適化と関係があるのではないかと思います。

24
adsamcik

多次元配列は、(n-1)次元の行列です。

したがって、int[,] square = new int[2,2]は正方行列2×2、int[,,] cube = new int [3,3,3]は立方体 - 正方行列3×3です。比例性は必要ありません。

ギザギザ配列は単なる配列の配列、つまり各セルに配列が含まれる配列です。

MDAは比例しているので、JDはそうではないかもしれません!各セルは任意の長さの配列を含むことができます。

14
abatishchev

これは上記の回答で言及されているかもしれませんが、明示的にではありません:ギザギザの配列では、array[row]を使用してデータの行全体を参照できますが、これはマルチd配列では許可されません。

7
lznt

他の答えに加えて、多次元配列がヒープ上の1つの大きな塊のオブジェクトとして割り当てられていることに注意してください。これにはいくつかの影響があります。

  1. 多次元配列の中には、それと同等のギザギザ配列に対応していないラージオブジェクトヒープ(LOH)に割り当てられるものがあります。
  2. 多次元配列を割り当てるには、GCは単一の連続した空きメモリブロックを見つける必要がありますが、ギザギザ配列はヒープフラグメンテーションに起因するギャップを埋めることができるかもしれません。しかし、LOHはデフォルトでは圧縮されません(あなたはそれを要求しなければなりません、そしてあなたはそれを望むたびに要求しなければなりません)。
  3. 多次元配列の場合は <gcAllowVeryLargeObjects> の方法を調べると、ジャグ配列のみを使用している場合に問題が発生する可能性があります。
2
Joe Amenta

変換を行うために使用するアセンブリ、クラス、メソッド、およびストアドプロシージャのデータベースを構築するために、ildasmによって生成された.ilファイルを解析しています。私は次のようなことに出会いました。

.method private hidebysig instance uint32[0...,0...] 
        GenerateWorkingKey(uint8[] key,
                           bool forEncryption) cil managed

2006年に出版された、Serge Lidin著、著書 『Expert .NET 2.0 IL Assembler』、第8章、プリミティブ型とシグニチャー、149-150頁。

<type>[]<type>のベクトルと呼ばれます。

<type>[<bounds> [<bounds>**] ]<type>の配列と呼ばれます

**は繰り返すことができ、[ ]はオプションであることを意味します。

例:<type> = int32とします。

1)int32[...,...]は、未定義の下限とサイズの2次元配列です。

2)int32[2...5]は、下限2、サイズ4の1次元配列です。

3)int32[0...,0...]は、下限が0でサイズが未定義の2次元配列です。

トム

2