C#中多维数组和数组数组之间有什么区别?

What are the differences between a multidimensional array and an array of arrays in C#?

在C中,多维数组double[,]和数组double[][]有什么区别?

如果有区别,每种方法的最佳用途是什么?


阵列(锯齿状阵列)比多维阵列更快,可以更有效地使用。多维数组有更好的语法。

如果您使用锯齿状和多维数组编写一些简单的代码,然后使用IL反汇编程序检查编译的程序集,您将看到锯齿状(或一维)数组的存储和检索是简单的IL指令,而多维数组的相同操作是始终较慢的方法调用。

考虑以下方法:

1
2
3
4
5
6
7
8
9
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如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.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实现,它们不是。

1
2
3
 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

第一行是交错数组的计时,第二行是多维数组,第三行是多维数组,应该是这样的。程序如下所示,仅供参考,这是运行mono测试的。(Windows时间安排大不相同,主要是由于CLR实现的变化)。

在Windows上,交错数组的时间安排非常优越,这与我自己对多维数组查找应该是什么样子的解释大致相同,请参见"single()"。遗憾的是,Windows JIT编译器真的很蠢,这让性能讨论变得困难,有太多的不一致之处。

这些是我在Windows上得到的时间安排,这里是相同的处理,第一行是锯齿形数组,第二行是多维的,第三行是我自己的多维实现,注意这在Windows上比Mono慢多少。

1
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

源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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中的表。数组(交错数组)允许您让每个元素保存另一个具有相同类型可变长度的数组。

因此,如果您确定数据的结构类似于表(固定的行/列),则可以使用多维数组。锯齿形数组是固定元素,每个元素可以容纳可变长度的数组

例如,psuedocode:

1
2
3
4
5
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
1 | 2
3 | 4
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
 1 |  2 |  3 | 4
11 | 12
21 | 22 | 23


前言:这篇评论是为了解决奥古坦提供的答案,但由于苏的愚蠢的声誉系统,我不能把它张贴在它所属的地方。

您断言一个比另一个慢,因为方法调用不正确。一种比另一种慢,因为边界检查算法更复杂。您可以通过查看(而不是IL)编译的程序集轻松地验证这一点。例如,在我的4.5安装中,访问存储在ECX指向的二维数组中的元素(通过EDX中的指针),索引存储在EAX和EDX中,如下所示:

1
2
3
4
5
6
7
8
9
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和JMP,代码几乎可以解析为(x*y_max+y)*sizeof(ptr)+sizeof(array_header)。这个计算速度和随机访问一个元素的速度一样快(一个乘法可以用移位代替,因为这就是我们选择字节大小作为两位幂的全部原因)。

另一个复杂的问题是,在许多情况下,现代编译器会在迭代单个维度数组时,对元素访问的嵌套边界检查进行优化。结果就是代码基本上只是在数组的连续内存上前进一个索引指针。多维数组上的简单迭代通常涉及一个额外的嵌套逻辑层,因此编译器不太可能优化操作。因此,尽管访问单个元素的边界检查开销在数组维度和大小方面逐渐分摊到常量运行时,但是测量差异的简单测试用例可能需要花费很多倍的时间才能执行。


我想对此进行更新,因为在.NET核心多维数组中,多维数组比锯齿状数组更快。我运行了John Leidegren的测试,这些是.NET核心2.0预览版2的结果。我增加了维度值,以减少后台应用程序可能产生的影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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]引用整行数据,但对于多维数组,这是不允许的。


除了其他答案之外,请注意,多维数组被分配为堆上的一个大而粗的对象。这有一些含义:

  • 一些多维数组将被分配到大型对象堆(LOH)上,否则它们的等效锯齿形数组对应项将无法分配。
  • GC需要找到一个连续的空闲内存块来分配多维数组,而锯齿状数组可能能够填补堆碎片造成的空白…由于压缩,这通常不是.NET中的一个问题,但是LOH在默认情况下不会被压缩(您必须请求它,并且每次需要它时都必须请求)。
  • 如果您只使用交错数组,那么在问题出现之前,您需要先查看中的多维数组。

  • 我正在分析由ildasm生成的.il文件,以构建用于进行转换的assemblies、类、方法和存储过程的数据库。我遇到了以下问题,这打破了我的分析。

    1
    2
    3
    .method private hidebysig instance uint32[0...,0...]
            GenerateWorkingKey(uint8[] key,
                               bool forEncryption) cil managed

    《专家.NET 2.0 IL汇编程序》,作者:SergeLidin,Apress,2006年出版,第8章,原始类型和签名,第149-150页。

    []被称为的矢量,

    [ [**] ]被称为的数组。

    **表示可以重复,[ ]表示可选。

    示例:让 = int32

    1)int32[...,...]是一个不确定下限和大小的二维数组。

    2)int32[2...5]是一个下界为2、大小为4的一维数组。

    3)int32[0...,0...]是一个下界为0、大小不确定的二维数组。

    汤姆