490

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

違いがある場合、それぞれの最適な使用法は何ですか?

4

12 に答える 12

351

配列の配列(ジャグ配列)は、多次元配列よりも高速であり、より効果的に使用できます。多次元配列の構文はより優れています。

ギザギザの多次元配列を使用して単純なコードを記述し、コンパイルされたアセンブリをIL逆アセンブラで検査すると、ギザギザ(または1次元)配列の格納と取得は単純な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でさえ、プロジェクトの分析に多次元配列を使用する場合は、多次元配列ではなくジャグ配列を使用する必要があると言われています。

于 2009-02-28T08:07:08.977 に答える
203

多次元配列は優れたリニア メモリ レイアウトを作成しますが、ジャグ配列はいくつかの余分なレベルの間接性を意味します。

jagged[3][6]ジャグ配列の値var jagged = new int[10][5]を調べるには、次のようにします。インデックス 3 の要素 (配列) を調べ、その配列のインデックス 6 の要素 (値) を調べます。この場合、次元ごとに追加のルックアップがあります (これは、コストのかかるメモリ アクセス パターンです)。

多次元配列はメモリ内に線形に配置され、実際の値はインデックスを乗算することによって求められます。ただし、 array を指定すると、その多次元配列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].Lengthはありませんjagged[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 

最初の行はジャグ配列のタイミング、2 番目の行は多次元配列、3 番目の行はそうあるべきです。プログラムを以下に示します。参考までに、これは mono を実行してテストされました。(主に CLR 実装の違いにより、ウィンドウのタイミングは大きく異なります)。

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

これらは私がウィンドウで得たタイミングです。ここでも同じです。最初の行はギザギザの配列、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();
    }
}
于 2009-02-28T09:34:49.377 に答える
76

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

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

例:擬似コード:

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

上記を2x2テーブルと考えてください。

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
于 2009-02-28T08:22:18.380 に答える
49

序文:このコメントは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]

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

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

于 2013-10-28T16:23:47.340 に答える
15

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

int[,] square = new int[2,2]正方行列 2x2もint[,,] cube = new int [3,3,3]立方体 - 正方行列 3x3 です。比例は必要ありません。

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

つまり、MDA は比例していますが、JD は比例していない可能性があります。各セルには、任意の長さの配列を含めることができます!

于 2009-02-28T20:23:18.223 に答える