C#中多维数组double[,]
和数组数组double[][]
之间有什么区别?
如果存在差异,每种产品的最佳用途是什么?
答案 0 :(得分:310)
数组(锯齿状数组)比多维数组更快,可以更有效地使用。多维数组具有更好的语法。
如果使用锯齿状和多维数组编写一些简单的代码,然后使用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也告诉我,当你用它来分析你的项目时,应该使用锯齿状数组而不是多维数。
答案 1 :(得分:189)
多维数组创建了一个漂亮的线性内存布局,而锯齿状数组则暗示了几个额外的间接层。
在锯齿状数组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上获得的时间,在这里同样处理,第一行是锯齿状数组,第二行是多维,第三行是我自己的多维实现,请注意,与单声道相比,它在窗口上的速度要慢得多。
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 :(得分:65)
简单地说,多维数组类似于DBMS中的表 Array of Array(锯齿状数组)允许您让每个元素保持另一个具有相同类型可变长度的数组。
因此,如果您确定数据结构看起来像一个表(固定行/列),则可以使用多维数组。锯齿状阵列是固定元素和每个元素都可以包含一个可变长度的数组
E.g。伪码:
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
答案 3 :(得分:38)
前言:此评论旨在解决the answer provided by 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)
。这个计算速度一样快(一个乘法可以用一个移位代替,因为这就是我们选择字节大小为两位幂的全部原因),就像随机访问一个元素一样。
另一个复杂因素是,在很多情况下,现代编译器会在迭代单维数组时优化嵌套边界检查元素访问。结果是代码基本上只是将索引指针推进到数组的连续内存上。对多维数组进行简单的迭代通常涉及额外的嵌套逻辑层,因此编译器不太可能优化操作。因此,即使访问单个元素的边界检查开销在数组维度和大小方面分摊到常量运行时,测量差异的简单测试用例可能需要花费很长时间才能执行。
答案 4 :(得分:24)
我想对此进行更新,因为在 .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上的某些优化有关
答案 5 :(得分:14)
多维数组是(n-1)维数矩阵。
所以int[,] square = new int[2,2]
是方阵2x2,int[,,] cube = new int [3,3,3]
是一个方块矩阵3x3。不需要比例。
锯齿状数组只是数组数组 - 每个单元格包含一个数组的数组。
所以MDA是成比例的,JD可能不是!每个单元格可以包含一个任意长度的数组!
答案 6 :(得分:7)
这可能已在上面的答案中提到但未明确说明:使用锯齿状数组,您可以使用array[row]
来引用整行数据,但这不适用于多维数组。
答案 7 :(得分:3)
我想我会在未来与 .NET 5 的一些性能结果相提并论,因为这将是从现在开始每个人都使用的平台。
这些测试与 John Leidegren 使用的测试相同(2009 年)。
我的结果 (.NET 5.0.1):
Debug:
(Jagged)
5.616 4.719 4.778 5.524 4.559 4.508 5.913 6.107 5.839 5.270
(Multi)
6.336 7.477 6.124 5.817 6.516 7.098 5.272 6.091 25.034 6.023
(Single)
4.688 3.494 4.425 6.176 4.472 4.347 4.976 4.754 3.591 4.403
Release(code optimizations on):
(Jagged)
2.614 2.108 3.541 3.065 2.172 2.936 1.681 1.724 2.622 1.708
(Multi)
3.371 4.690 4.502 4.153 3.651 3.637 3.580 3.854 3.841 3.802
(Single)
1.934 2.102 2.246 2.061 1.941 1.900 2.172 2.103 1.911 1.911
在 6 核 3.7GHz AMD Ryzen 1600 机器上运行。
看起来性能比仍然大致相同。我会说除非你真的很努力优化,否则只需使用多维数组,因为语法更容易使用。
答案 8 :(得分:2)
我正在解析由ildasm生成的.il文件,以构建用于执行转换的程序集,类,方法和存储过程的数据库。我遇到了以下内容,这破坏了我的解析。
user> (defn map-into-2 [f coll]
(reduce #(assoc %1 %2 (f %2)) {} coll))
#'user/map-into-2
user> (map-into-2 inc [1 2 3])
;;=> {1 2, 2 3, 3 4}
专家.NET 2.0 IL汇编程序,由Serge Lidin,Apress,2006年出版,第8章,原始类型和签名,第149-150页解释。
.method private hidebysig instance uint32[0...,0...]
GenerateWorkingKey(uint8[] key,
bool forEncryption) cil managed
被称为<type>[]
,
<type>
被称为<type>[<bounds> [<bounds>**] ]
<type>
表示可能会重复,**
表示可选。
示例:允许[ ]
。
1)<type> = int32
是未定义的下界和大小的二维数组
2)int32[...,...]
是下界2和大小4的一维数组。
3)int32[2...5]
是下界0和未定义大小的二维数组。
汤姆
答案 9 :(得分:2)
除了其他答案之外,请注意多维数组被分配为堆上的一个大块子对象。这有一些含义:
<gcAllowVeryLargeObjects>
多维数组方式。答案 10 :(得分:0)
使用基于 John Leidegren 的测试,我使用 .NET 4.7.2 对结果进行了基准测试,这是与我的目的相关的版本,并认为我可以分享。我最初是从 dotnet 核心 GitHub 存储库中的 this comment 开始的。
随着阵列大小的变化,性能似乎变化很大,至少在我的设置中,1 个处理器至强处理器和 4 个物理 8 个逻辑。
w = 初始化一个数组,并将 int i * j 放入其中。 wr = do w,然后在另一个循环中将 int x 设置为 [i,j]
随着数组大小的增长,多维似乎表现更好。
尺寸 | rw | 方法 | 平均 | 错误 | StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | 分配的内存/操作 |
---|---|---|---|---|---|---|---|---|---|
1800*500 | w | 锯齿状 | 2.445 毫秒 | 0.0959 毫秒 | 0.1405 毫秒 | 578.1250 | 281.2500 | 85.9375 | 3.46 MB |
1800*500 | w | Multi | 3.079 毫秒 | 0.2419 毫秒 | 0.3621 毫秒 | 269.5313 | 269.5313 | 269.5313 | 3.43 MB |
2000*4000 | w | 锯齿状 | 50.29 毫秒 | 3.262 毫秒 | 4.882 毫秒 | 5937.5000 | 3375.0000 | 937.5000 | 30.62 MB |
2000*4000 | w | Multi | 26.34 毫秒 | 1.797 毫秒 | 2.690 毫秒 | 218.7500 | 218.7500 | 218.7500 | 30.52 MB |
2000*4000 | wr | 锯齿状 | 55.30 毫秒 | 3.066 毫秒 | 4.589 毫秒 | 5937.5000 | 3375.0000 | 937.5000 | 30.62 MB |
2000*4000 | wr | Multi | 32.23 毫秒 | 2.798 毫秒 | 4.187 毫秒 | 285.7143 | 285.7143 | 285.7143 | 30.52 MB |
1000*2000 | wr | 锯齿状 | 11.18 毫秒 | 0.5397 毫秒 | 0.8078 毫秒 | 1437.5000 | 578.1250 | 234.3750 | 7.69 MB |
1000*2000 | wr | Multi | 6.622 毫秒 | 0.3238 毫秒 | 0.4847 毫秒 | 210.9375 | 210.9375 | 210.9375 | 7.63 MB |
更新:最后两个测试使用 double[,] 而不是 int[,]。考虑到错误,差异似乎很重要。使用 int 时,锯齿状与 md 的均值比介于 1.53 倍和 1.86 倍之间,双倍时则为 1.88 倍和 2.42 倍。
尺寸 | rw | 方法 | 平均 | 错误 | StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | 分配的内存/操作 |
---|---|---|---|---|---|---|---|---|---|
1000*2000 | wr | 锯齿状 | 26.83 毫秒 | 1.221 毫秒 | 1.790 毫秒 | 3062.5000 | 1531.2500 | 531.2500 | 15.31 MB |
1000*2000 | wr | Multi | 12.61 毫秒 | 1.018 毫秒 | 1.524 毫秒 | 156.2500 | 156.2500 | 156.2500 | 15.26 MB |