回答:
配列の配列(ジャグ配列)は、多次元配列よりも高速で、より効果的に使用できます。多次元配列の方が構文が優れています。
ギザギザの多次元配列を使用して簡単なコードを記述し、コンパイルされたアセンブリを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でも、プロジェクトを分析するときに多次元配列の代わりにギザギザ配列を使用するように指示されています。
多次元配列は素晴らしい線形メモリレイアウトを作成しますが、ギザギザの配列はいくつかの追加レベルの間接指定を意味します。
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();
}
}
簡単に言えば、多次元配列は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
序文:このコメントは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次元配列を反復処理しながら要素アクセスのネストされた境界チェックを最適化しないケースがたくさんあることです。結果は、基本的に配列の連続したメモリ上でインデックスポインターを進めるコードです。多次元配列に対する単純な反復では、通常、ネストされたロジックの追加のレイヤーが含まれるため、コンパイラーが操作を最適化する可能性は低くなります。したがって、単一の要素にアクセスすることによる境界チェックのオーバーヘッドは、配列の次元とサイズに関して一定のランタイムに償却されますが、違いを測定する単純なテストケースは、実行に何倍も時間がかかる場合があります。
.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で行われた最適化に関係していると思います
多次元配列は(n-1)次元の行列です。
だから、int[,] square = new int[2,2]
正方行列の2×2であり、int[,,] cube = new int [3,3,3]
正方行列3x3の-キューブがあります。比例は必要ありません。
ギザギザの配列は、配列の配列(各セルに配列が含まれる配列)です。
したがって、MDAは比例しますが、JDは比例しないことがあります。各セルには、任意の長さの配列を含めることができます。
他の回答に加えて、多次元配列がヒープ上の1つの大きなチャンクオブジェクトとして割り当てられていることに注意してください。これにはいくつかの意味があります:
<gcAllowVeryLargeObjects>
多次元配列のための方法あなただけの今までにギザギザの配列を使用した場合、問題がこれまでに出てくる前に。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次元配列です。
トム
double[,]
は長方形配列double[][]
ですが、「ギザギザ配列」として知られています。最初の行は各行に同じ数の「列」があり、2番目の行は(可能性として)各行に異なる数の「列」があります。