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


454

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

如果存在差异,那么每个最好的用途是什么?


7
第一个double[,]是矩形阵列,而double[][]称为“锯齿状阵列”。第一行每行具有相同数量的“列”,而第二行(潜在地)每行具有不同数量的“列”。
GreatAndPowerfulOz

Answers:


334

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

如果您使用锯齿形和多维数组编写一些简单的代码,然后使用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告诉您,在使用锯齿状数组分析项目时也应使用锯齿状数组,而不是多维数组。


7
@John,自己测量一下,不要做任何假设。
Hosam Aly

2
@John:我的第一反应也是,但我错了-有关详细信息,请参阅Hosams问题。
亨克·霍尔特曼

38
多维数组在逻辑上应该更有效,但JIT编译器无法实现。上面的代码没有用,因为它没有循环显示数组访问。
ILoveFortran

3
@Henk Holterman-请参阅下面的答案,可能是窗口锯齿状阵列的速度很快,但人们必须意识到这完全是CLR特定的,而不是单声道的情况……
John Leidegren 09年

12
我知道这是一个古老的问题,只是想知道自从提出此问题以来,CLR是否已针对多维数组进行了优化。
安东尼·尼科尔斯

197

多维数组创建了一个不错的线性内存布局,而锯齿状数组则暗示了额外的间接级别。

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();
    }
}

2
尝试自己计时,看看两者的表现如何。锯齿状数组在.NET中进行了更优化。它可能与边界检查有关,但是无论原因如何,时间和基准都清楚地表明,锯齿状数组的访问速度比多维数组要快。
Hosam Aly

10
但是您的时间安排似乎太小(几毫秒)。在此级别上,您将受到系统服务和/或驱动程序的很大干扰。使您的测试更大,至少要花一两秒钟。
Hosam Aly

8
@JohnLeidegren:半个世纪以来,人们就已经认识到多维数组在索引一个维时比另一个维更好的事实,因为只有一个特定维不同的元素将连续存储在内存中,并且使用多种类型的内存(过去和当前),访问连续项目要比访问远处项目快。我认为在.net中,您应该通过最后一个下标来获得最佳结果索引,这是您正在做的事情,但是无论如何,测试下标交换的时间可能会提供很多信息。
2012年

16
@supercat:C#中的多维数组以行优先顺序存储,交换下标的顺序会比较慢,因为您将非连续地访问内存。BTW报告的时间不再是准确的,我得到几乎快一倍时间多维数组不是参差不齐阵列(在最新的.NET CLR测试),这是怎么它应该是...
荷银

9
我知道这有点古怪,但是我不得不提到这不是Windows vs Mono,而是CLR vs Mono。有时您似乎会混淆这些。两者不相等。Mono也可以在Windows上使用。
Magus 2014年

70

简单地说,多维数组类似于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

4
这才是决定使用什么时真正重要的。.并不是这个速度会很..当您拥有平方阵列时,好的速度可能会成为一个因素。
Xaser

45

前言:此评论旨在解决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)。这种计算与随机访问元素的速度一样快(一个乘法可以由一个移位代替,因为这就是我们选择将字节大小设置为两位的幂的全部原因)。

另一个复杂因素是,在很多情况下,现代编译器将优化嵌套嵌套边界检查,同​​时在单维数组上进行迭代。结果是代码基本上只将索引指针移到数组的连续内存上。多维数组上的幼稚迭代通常会涉及到嵌套逻辑的额外一层,因此编译器不太可能优化操作。因此,即使访问单个元素的边界检查开销在数组维数和大小方面均摊到了恒定的运行时间上,但用于测量差异的简单测试用例执行起来却可能会花费很多时间。


1
感谢您纠正okutane(不是Dmitry)的答案。令人讨厌的是,人们在Stackoverflow上给出了错误的答案并获得了250票赞成票,而其他人给出了正确的答案而得到的票数却少得多。但最后,IL代码无关紧要。您必须真正衡量速度才能对性能发表任何评论。是你做的吗?我认为这种差异将是荒谬的。
Elmue

38

我想对此进行更新,因为在.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上的某些优化有关


14

多维数组是(n-1)维矩阵。

int[,] square = new int[2,2]方矩阵2x2 int[,,] cube = new int [3,3,3]也是如此,是一个立方体-方矩阵3x3。不需要比例。

锯齿状的数组只是数组的数组-每个单元格都包含一个数组的数组。

所以MDA是成比例的,JD可能不是!每个单元格可以包含任意长度的数组!


7

上面的答案中可能已经提到了这一点,但没有明确提及:锯齿状数组可以array[row]用来引用整行数据,但是multi-d数组则不允许这样做。


4

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

  1. 一些多维数组将在大对象堆(LOH)上分配,否则它们对应的锯齿数组副本将没有。
  2. GC将需要找到一个连续的空闲内存块来分配多维数组,而锯齿状的数组可能能够填补由堆碎片引起的空白...由于压缩,这在.NET中通常不是问题,但默认情况下不会对LOH进行压缩(您必须提出要求,并且每次都需要询问)。
  3. 你会想看看<gcAllowVeryLargeObjects>多维数组的方式的问题都不会拿出如果你只曾经使用交错数组之前。

2

我正在解析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和未定义大小的二维数组。

汤姆

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.