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

时间:2009-02-28 07:55:42

标签: c# multidimensional-array jagged-arrays

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

如果存在差异,每种产品的最佳用途是什么?

11 个答案:

答案 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)

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

  1. 一些多维数组将在大对象堆(LOH)上分配,否则它们的等效锯齿数组不会有。
  2. GC需要找到一个连续的空闲内存块来分配一个多维数组,而一个锯齿状数组可能能够填补由堆碎片引起的空白......这在.NET中通常不是问题因为压缩,但LOH默认不会被压缩(你必须要求它,你必须每次都要求它)。
  3. 如果您只使用锯齿状数组,那么在问题出现之前,您需要查看<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