C#の多次元配列double[,]
と配列配列double[][]
の違いは何ですか?
違いがある場合、それぞれのための最良の用途は何ですか?
配列の配列(ギザギザ配列)は多次元配列よりも高速であり、より効果的に使用できます。多次元配列はより良い構文を持っています。
ギザギザ配列と多次元配列を使用して単純なコードを作成し、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でも、プロジェクトを分析するときに多次元配列の代わりにギザギザ配列を使用することをお勧めします。
多次元配列は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].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
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();
}
}
簡単に言うと、多次元配列は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
序文:このコメントは 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つの要素にアクセスすることによる境界チェックのオーバーヘッドが、配列のサイズとサイズに関して一定の実行時間で償却されても、違いを測定するための単純なテストケースの実行には何倍も時間がかかります。
。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の最適化と関係があるのではないかと思います。
多次元配列は、(n-1)次元の行列です。
したがって、int[,] square = new int[2,2]
は正方行列2×2、int[,,] cube = new int [3,3,3]
は立方体 - 正方行列3×3です。比例性は必要ありません。
ギザギザ配列は単なる配列の配列、つまり各セルに配列が含まれる配列です。
MDAは比例しているので、JDはそうではないかもしれません!各セルは任意の長さの配列を含むことができます。
これは上記の回答で言及されているかもしれませんが、明示的にではありません:ギザギザの配列では、array[row]
を使用してデータの行全体を参照できますが、これはマルチd配列では許可されません。
他の答えに加えて、多次元配列がヒープ上の1つの大きな塊のオブジェクトとして割り当てられていることに注意してください。これにはいくつかの影響があります。
<gcAllowVeryLargeObjects>
の方法を調べると、ジャグ配列のみを使用している場合に問題が発生する可能性があります。変換を行うために使用するアセンブリ、クラス、メソッド、およびストアドプロシージャのデータベースを構築するために、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次元配列です。
トム