Answers:
数组数组(锯齿状数组)比多维数组更快,并且可以更有效地使用。多维数组具有更好的语法。
如果您使用锯齿形和多维数组编写一些简单的代码,然后使用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
第一行是锯齿状数组的时间,第二行显示多维数组,第三行是应该的。该程序如下所示,仅供参考,这是经过测试运行的单声道。(Windows的时间差异很大,主要是由于CLR实现的变化)。
在Windows上,锯齿状数组的时序非常优越,与我自己对多维数组查找的解释相同,请参见“ Single()”。遗憾的是,Windows JIT编译器确实很愚蠢,这不幸地使这些性能讨论变得困难,存在太多的不一致之处。
这些是我在Windows上获得的时间,在这里也是一样,第一行是锯齿状的数组,第二排是多维的,第三排是我自己的多维实现,请注意,与mono相比,这在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而不是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上的某些优化有关
除了其他答案,请注意,多维数组被分配为堆上的一个大块对象。这具有一些含义:
<gcAllowVeryLargeObjects>
多维数组的方式的问题都不会拿出如果你只曾经使用交错数组之前。我正在解析ildasm生成的.il文件,以建立一个包含组件,类,方法和存储过程的数据库,以进行转换。我遇到了以下内容,这破坏了我的解析。
.method private hidebysig instance uint32[0...,0...]
GenerateWorkingKey(uint8[] key,
bool forEncryption) cil managed
Serge Lidin,Apress出版的《 Expert .NET 2.0 IL汇编器》一书于2006年出版,第8章,原始类型和签名,第149-150页。
<type>[]
被称为组成的矢量<type>
,
<type>[<bounds> [<bounds>**] ]
被称为数组 <type>
**
手段可以重复,[ ]
手段可选。
示例:让<type> = int32
。
1)int32[...,...]
是未定义下限和大小的二维数组
2)int32[2...5]
是下界2和大小4的一维数组。
3)int32[0...,0...]
是下界0和未定义大小的二维数组。
汤姆
double[,]
是矩形阵列,而double[][]
称为“锯齿状阵列”。第一行每行具有相同数量的“列”,而第二行(潜在地)每行具有不同数量的“列”。