C#中多维数组double[,]
和数组数组之间有什么区别double[][]
?
如果有差异,每个人的最佳用途是什么?
阵列阵列(锯齿状阵列)比多维阵列更快,可以更有效地使用.多维数组具有更好的语法.
如果使用锯齿状和多维数组编写一些简单的代码然后使用IL反汇编程序检查编译的程序集,您将看到从锯齿状(或单维)数组中存储和检索是简单的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
多维数组的属性返回元素的总数,即10*30 = 300.
Rank
锯齿状数组的属性始终为1,但多维数组可以具有任何等级.GetLength
任何数组的方法都可用于获取每个维度的长度.对于此示例中的多维数组,mult.GetLength(1)
返回30.
索引多维数组更快.例如,假设此示例中的多维数组mult[1,7]
= 30*1 + 7 = 37,则获取该索引37处的元素.这是更好的内存访问模式,因为只涉及一个内存位置,这是数组的基址.
因此,多维数组分配连续的内存块,而锯齿状数组不必是正方形,例如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
第一行是锯齿状阵列的时序,第二行是多维数组,第三行是应该如何.该程序如下所示,FYI测试运行单声道.(窗口时序差异很大,主要是由于CLR实现的变化).
在窗口上,锯齿状阵列的时序非常优越,与我自己对多维数组查找的解释大致相同,请参阅'Single()'.可悲的是,Windows JIT编译器真的很愚蠢,不幸的是这使得这些性能讨论变得困难,存在太多的不一致.
这些是我在windows上获得的时间,在这里同样处理,第一行是锯齿状数组,第二行是多维,第三行是我自己的多维实现,请注意这与Windows相比在Windows上慢了多少.
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(锯齿状数组)允许您让每个元素保持另一个相同类型的可变长度的数组.
因此,如果您确定数据结构看起来像一个表(固定行/列),则可以使用多维数组.锯齿状数组是固定元素,每个元素可以包含可变长度的数组
例如Psuedocode:
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安装中,访问存储在ecx指向的二维数组中的元素(通过edx中的指针),其中索引存储在eax和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)
.这个计算速度一样快(一个乘法可以用一个移位代替,因为这就是我们选择字节大小为两位幂的全部原因),就像随机访问一个元素一样.
另一个复杂因素是,在很多情况下,现代编译器会在迭代单维数组时优化嵌套边界检查元素访问.结果是代码基本上只是将索引指针推进到数组的连续内存上.对多维数组进行简单的迭代通常涉及额外的嵌套逻辑层,因此编译器不太可能优化操作.因此,即使访问单个元素的边界检查开销在数组维度和大小方面分摊到常量运行时,测量差异的简单测试用例可能需要花费很长时间才能执行.
我想对此进行更新,因为在.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]
是方阵2x2的,int[,,] cube = new int [3,3,3]
是一个立方体-方阵3x3的.不需要比例.
锯齿状数组只是数组的数组 - 每个单元格包含一个数组的数组.
所以MDA是成比例的,JD可能不是!每个单元格可以包含任意长度的数组!
这可能在上面的答案中已经提到过,但没有明确说明:使用锯齿状数组可以array[row]
用来引用整行数据,但这不适用于多维数组.