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


454

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

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


7
最初double[,]は長方形配列double[][]ですが、「ギザギザ配列」として知られています。最初の行は各行に同じ数の「列」があり、2番目の行は(可能性として)各行に異なる数の「列」があります。
GreatAndPowerfulOz

回答:


334

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

ギザギザの多次元配列を使用して簡単なコードを記述し、コンパイルされたアセンブリを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でも、プロジェクトを分析するときに多次元配列の代わりにギザギザ配列を使用するように指示されています。


7
@John、それらを自分で測定し、仮定をしないでください。
Hosam Aly、

2
@ジョン:私の最初の反応も私は間違っていました-詳細はHosamsの質問を見てください。
Henk Holterman、

38
多次元配列は論理的にはより効率的ですが、JITコンパイラによる実装はそうではありません。上記のコードは、ループ内の配列アクセスを示していないため、役に立ちません。
ILoveFortran 2009

3
@ヘンクホルターマン-以下の私の回答を参照してください。Windowsではギザギザの配列が高速である可能性がありますが、これは完全にCLR固有であり、たとえばモノラルの場合ではないことに
注意する必要があり

12
私はこれが古い質問であることを知っています。この質問が出されて以来、CLRが多次元配列用に最適化されているのかと思っているだけです。
アンソニーニコルズ

197

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

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

多次元配列はメモリ内に直線的に配置され、実際の値はインデックスを掛け合わせることによって求められます。ただし、配列を指定するvar mult = new int[10,30]と、Lengthその多次元配列の性質は、要素の総数は300 = 10×30、すなわち返します。

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番目の行はそうあるべきです。プログラムを以下に示します。参考までに、これはモノを実行してテストされました。(ウィンドウのタイミングは大きく異なりますが、主に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();
    }
}

2
自分でタイミングを調整してみて、両方のパフォーマンスを確認してください。ギザギザの配列は、.NETではより最適化されています。境界チェックに関連している可能性がありますが、理由に関係なく、タイミングとベンチマークは、ギザギザの配列が多次元配列よりも高速にアクセスできることを明確に示しています。
Hosam Aly、

10
しかし、タイミングが小さすぎるようです(数ミリ秒)。このレベルでは、システムサービスやドライバからの干渉が多くなります。テストをはるかに大きくし、少なくとも1〜2秒かかります。
Hosam Aly

8
@JohnLeidegren:1つの特定の次元のみが異なる要素はメモリに連続して保存され、多くのタイプのメモリ(過去そして存在する)、連続したアイテムへのアクセスは遠くのアイテムへのアクセスよりも高速です。私は.netではあなたがやっていたことである最後の添え字で最適な結果のインデックスを取得するべきだと思いますが、添え字を交換して時間をテストすることはどんな場合でも有益かもしれません。
スーパーキャット2012

16
@supercat:C#の多次元配列は行優先で格納されます。メモリに非連続的にアクセスするため、添え字の順序の入れ替えは遅くなります。ところで報告時間は..私はそれがあるべき方法である、ギザギザアレイ(最新の.NET CLR上でテスト)より多次元配列のための高速な時間のほぼ2倍取得、もはや正確でない
アムロ

9
私はこれが少し面倒なことを知っていますが、これはWindows対Monoではなく、CLR対Monoであることを言及する必要があります。あなたは時々それらを混乱させるように見えます。2つは同等ではありません。MonoはWindowsでも動作します。
メイガス2014年

70

簡単に言えば、多次元配列は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

4
これは、何を使用するかを決めるときに本当に重要なことです。この速度ではありません。正方形の配列を使用している場合は、速度が要素になることがあります。
Xaser

46

序文:このコメントは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次元配列を反復処理しながら要素アクセスのネストされた境界チェックを最適化しないケースがたくさんあることです。結果は、基本的に配列の連続したメモリ上でインデックスポインターを進めるコードです。多次元配列に対する単純な反復では、通常、ネストされたロジックの追加のレイヤーが含まれるため、コンパイラーが操作を最適化する可能性は低くなります。したがって、単一の要素にアクセスすることによる境界チェックのオーバーヘッドは、配列の次元とサイズに関して一定のランタイムに償却されますが、違いを測定する単純なテストケースは、実行に何倍も時間がかかる場合があります。


1
(Dmitryではなく)okutaneの回答を訂正していただき、ありがとうございます。Stackoverflowで間違った回答をして250の賛成票を獲得する一方で、他の人が正しい答えを提供してはるかに少ないことは不愉快です。しかし、結局のところ、ILコードは無関係です。パフォーマンスについて何かを言うには、速度を実際に測定する必要があります。あなたはそれをやりました?違いはばかげていると思います。
Elmue

38

.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の命令が必要

1次元配列が多次元よりも高速である理由を特定することはできませんでしたが、CPUで行われた最適化に関係していると思います


14

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

だから、int[,] square = new int[2,2]正方行列の2×2であり、int[,,] cube = new int [3,3,3]正方行列3x3の-キューブがあります。比例は必要ありません。

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

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


7

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


4

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

  1. 一部の多次元配列は、同等のぎざぎざの配列の対応物がなければ対応できないラージオブジェクトヒープ(LOH)に割り当てられます。
  2. 多次元配列を割り当てるには、GCが単一の連続した空きメモリブロックを見つける必要がありますが、ギザギザの配列は、ヒープの断片化によって生じたギャップを埋めることができる場合があります...圧縮のため、これは通常.NETの問題ではありません。 、しかしLOHはデフォルトでは圧縮されません(あなたはそれを要求する必要があり、あなたはそれを望むたびに尋ねなければなりません)。
  3. あなたはに見たいと思うでしょう<gcAllowVeryLargeObjects>多次元配列のための方法あなただけの今までにギザギザの配列を使用した場合、問題がこれまでに出てくる前に。

2

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

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

本、エキスパート.NET 2.0 ILアセンブラー、セルジュリディン、Apress、発行2006年、第8章、プリミティブ型と署名、pp。149-150は説明します。

<type>[]のベクターと呼ばれ<type>

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

**繰り返すことを[ ]意味し、オプションを意味します。

例:Let <type> = int32

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

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

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

トム

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.