【问题标题】:Why are multi-dimensional arrays in .NET slower than normal arrays?为什么 .NET 中的多维数组比普通数组慢?
【发布时间】:2010-10-02 21:37:31
【问题描述】:

编辑:我向大家道歉。当我实际上想说“多维数组”时,我使用了“锯齿状数组”一词(如下面的示例所示)。对于使用错误的名称,我深表歉意。我实际上发现锯齿状数组比多维数组更快!我已经添加了锯齿状数组的测量值。

我今天尝试使用 jagged 多维数组时,发现它的性能不如我预期。使用一维数组并手动计算索引比使用二维数组快得多(几乎是两倍)。我使用1024*1024 数组(初始化为随机值)编写了一个测试,进行了 1000 次迭代,我在我的机器上得到了以下结果:

sum(double[], int): 2738 ms (100%)
sum(double[,]):     5019 ms (183%)
sum(double[][]):    2540 ms ( 93%)

这是我的测试代码:

public static double sum(double[] d, int l1) {
    // assuming the array is rectangular
    double sum = 0;
    int l2 = d.Length / l1;
    for (int i = 0; i < l1; ++i)
        for (int j = 0; j < l2; ++j)
            sum += d[i * l2 + j];
    return sum;
}

public static double sum(double[,] d) {
    double sum = 0;
    int l1 = d.GetLength(0);
    int l2 = d.GetLength(1);
    for (int i = 0; i < l1; ++i)
        for (int j = 0; j < l2; ++j)
            sum += d[i, j];
    return sum;
}

public static double sum(double[][] d) {
    double sum = 0;
    for (int i = 0; i < d.Length; ++i)
        for (int j = 0; j < d[i].Length; ++j)
            sum += d[i][j];
    return sum;
}

public static void Main() {
    Random random = new Random();
    const int l1  = 1024, l2 = 1024;
    double[ ] d1  = new double[l1 * l2];
    double[,] d2  = new double[l1 , l2];
    double[][] d3 = new double[l1][];

    for (int i = 0; i < l1; ++i) {
        d3[i] = new double[l2];
        for (int j = 0; j < l2; ++j)
            d3[i][j] = d2[i, j] = d1[i * l2 + j] = random.NextDouble();
    }
    //
    const int iterations = 1000;
    TestTime(sum, d1, l1, iterations);
    TestTime(sum, d2, iterations);
    TestTime(sum, d3, iterations);
}

进一步调查表明,第二种方法的 IL 比第一种方法大 23%。 (代码大小 68 与 52。)这主要是由于对 System.Array::GetLength(int) 的调用。编译器还会为 jagged 多维数组发出对 Array::Get 的调用,而它只是为简单数组调用 ldelem

所以我想知道,为什么通过多维数组访问比普通数组慢?我会假设编译器(或 JIT)会做一些类似于我在第一个方法中所做的事情,但事实并非如此。

您能帮我理解为什么会这样吗?


更新:按照 Henk Holterman 的建议,这里是 TestTime 的实现:

public static void TestTime<T, TR>(Func<T, TR> action, T obj,
                                   int iterations)
{
    Stopwatch stopwatch = Stopwatch.StartNew();
    for (int i = 0; i < iterations; ++i)
        action(obj);
    Console.WriteLine(action.Method.Name + " took " + stopwatch.Elapsed);
}

public static void TestTime<T1, T2, TR>(Func<T1, T2, TR> action, T1 obj1,
                                        T2 obj2, int iterations)
{
    Stopwatch stopwatch = Stopwatch.StartNew();
    for (int i = 0; i < iterations; ++i)
        action(obj1, obj2);
    Console.WriteLine(action.Method.Name + " took " + stopwatch.Elapsed);
}

【问题讨论】:

标签: .net performance arrays


【解决方案1】:

哪个最快取决于您的数组大小。

便于阅读的图片:

控制台结果:

// * Summary *

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.997 (1909/November2018Update/19H2)
Intel Core i7-6700HQ CPU 2.60GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.302
  [Host]        : .NET Core 3.1.6 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.31603), X64 RyuJIT
  .NET Core 3.1 : .NET Core 3.1.6 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.31603), X64 RyuJIT

Job=.NET Core 3.1  Runtime=.NET Core 3.1

|           Method |    D |            Mean |         Error |        StdDev |      Gen 0 |     Gen 1 |     Gen 2 |  Allocated |
|----------------- |----- |----------------:|--------------:|--------------:|-----------:|----------:|----------:|-----------:|
| 'double[D1][D2]' |   10 |        376.2 ns |       7.57 ns |      12.00 ns |     0.3643 |         - |         - |     1144 B |
| 'double[D1, D2]' |   10 |        325.5 ns |       3.71 ns |       3.47 ns |     0.2675 |         - |         - |      840 B |
| 'double[D1][D2]' |   50 |      4,821.4 ns |      44.71 ns |      37.34 ns |     6.8893 |         - |         - |    21624 B |
| 'double[D1, D2]' |   50 |      5,834.1 ns |      64.35 ns |      60.20 ns |     6.3629 |         - |         - |    20040 B |
| 'double[D1][D2]' |  100 |     19,124.4 ns |     230.39 ns |     454.77 ns |    26.2756 |    0.7019 |         - |    83224 B |
| 'double[D1, D2]' |  100 |     23,561.4 ns |     299.18 ns |     279.85 ns |    24.9939 |         - |         - |    80040 B |
| 'double[D1][D2]' |  500 |  1,248,458.7 ns |  11,241.19 ns |  10,515.01 ns |   322.2656 |  160.1563 |         - |  2016025 B |
| 'double[D1, D2]' |  500 |    966,940.8 ns |   5,694.46 ns |   5,326.60 ns |   303.7109 |  303.7109 |  303.7109 |  2000034 B |
| 'double[D1][D2]' | 1000 |  8,987,202.8 ns |  97,133.16 ns |  90,858.41 ns |  1421.8750 |  578.1250 |  265.6250 |  8032582 B |
| 'double[D1, D2]' | 1000 |  3,628,421.3 ns |  72,240.02 ns | 177,206.01 ns |   179.6875 |  179.6875 |  179.6875 |  8000036 B |
| 'double[D1][D2]' | 1500 | 26,496,994.4 ns | 380,625.25 ns | 356,037.09 ns |  3406.2500 | 1500.0000 |  531.2500 | 18048064 B |
| 'double[D1, D2]' | 1500 | 12,417,733.7 ns | 243,802.76 ns | 260,866.22 ns |   156.2500 |  156.2500 |  156.2500 | 18000038 B |
| 'double[D1][D2]' | 3000 | 86,943,097.4 ns | 485,339.32 ns | 405,280.31 ns | 12833.3333 | 7000.0000 | 1333.3333 | 72096325 B |
| 'double[D1, D2]' | 3000 | 57,969,405.9 ns | 393,463.61 ns | 368,046.11 ns |   222.2222 |  222.2222 |  222.2222 | 72000100 B |

// * Hints *
Outliers
  MultidimensionalArrayBenchmark.'double[D1][D2]': .NET Core 3.1 -> 1 outlier  was  removed (449.71 ns)
  MultidimensionalArrayBenchmark.'double[D1][D2]': .NET Core 3.1 -> 2 outliers were removed, 3 outliers were detected (4.75 us, 5.10 us, 5.28 us)
  MultidimensionalArrayBenchmark.'double[D1][D2]': .NET Core 3.1 -> 13 outliers were removed (21.27 us..30.62 us)
  MultidimensionalArrayBenchmark.'double[D1, D2]': .NET Core 3.1 -> 1 outlier  was  removed (4.19 ms)
  MultidimensionalArrayBenchmark.'double[D1, D2]': .NET Core 3.1 -> 3 outliers were removed, 4 outliers were detected (11.41 ms, 12.94 ms..13.61 ms)
  MultidimensionalArrayBenchmark.'double[D1][D2]': .NET Core 3.1 -> 2 outliers were removed (88.68 ms, 89.27 ms)

// * Legends *
  D         : Value of the 'D' parameter
  Mean      : Arithmetic mean of all measurements
  Error     : Half of 99.9% confidence interval
  StdDev    : Standard deviation of all measurements
  Gen 0     : GC Generation 0 collects per 1000 operations
  Gen 1     : GC Generation 1 collects per 1000 operations
  Gen 2     : GC Generation 2 collects per 1000 operations
  Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
  1 ns      : 1 Nanosecond (0.000000001 sec)

基准代码:

[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.NetCoreApp31)]
[MemoryDiagnoser]
public class MultidimensionalArrayBenchmark {
    [Params(10, 50, 100, 500, 1000, 1500, 3000)]
    public int D { get; set; }

    [Benchmark(Description = "double[D1][D2]")]
    public double[][] JaggedArray() {
        var array = new double[D][];
        for (int i = 0; i < array.Length; i++) {
            var subArray = new double[D];
            array[i] = subArray;

            for (int j = 0; j < subArray.Length; j++) {
                subArray[j] = j + i * 10;
            }
        }

        return array;
    }

    [Benchmark(Description = "double[D1, D2]")]
    public double[,] MultidimensionalArray() {
        var array = new double[D, D];
        for (int i = 0; i < D; i++) {
            for (int j = 0; j < D; j++) {
                array[i, j] = j + i * 10;
            }
        }

        return array;
    }
}

【讨论】:

  • 我运行了自己的快速测试(一些其他代码),Multi-Dim 总体上似乎更快。 multi-dim 更快是有道理的,并且在 2002 年学习 .net 时发现 Jagged 更快。尽管需要一大块连续的内存,但可以在 multi-dim 上进行更有效的数学运算。 .net 核心中似乎更快的是使用单个数组“int outValue = MyArray[x * DIM1 * DIM2 + y* DIM2 + z]”的自制版本。
【解决方案2】:

有趣的是,我从上面运行了以下代码 在 Vista 机器上使用 VS2008 NET3.5SP1 Win32, 在发布/优化中,差异几乎无法衡量, 而 debug/noopt 多暗阵列要慢得多。 (我两次运行这三个测试以减少对第二组的 JIT 影响。)

  Here are my numbers: 
    sum took 00:00:04.3356535
    sum took 00:00:04.1957663
    sum took 00:00:04.5523050
    sum took 00:00:04.0183060
    sum took 00:00:04.1785843 
    sum took 00:00:04.4933085

看第二组三个数字。 差异不足以让我在一维数组中编码所有内容。

虽然我没有发布它们,但在 Debug/unoptimized 中,多维 vs. 单一/锯齿状确实有很大的不同。

完整程序:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace single_dimension_vs_multidimension
{
    class Program
    {


        public static double sum(double[] d, int l1) {    // assuming the array is rectangular 
            double sum = 0; 
            int l2 = d.Length / l1; 
            for (int i = 0; i < l1; ++i)   
                for (int j = 0; j < l2; ++j)   
                    sum += d[i * l2 + j];   
            return sum;
        }

        public static double sum(double[,] d)
        {
            double sum = 0;  
            int l1 = d.GetLength(0);
            int l2 = d.GetLength(1);   
            for (int i = 0; i < l1; ++i)    
                for (int j = 0; j < l2; ++j)   
                    sum += d[i, j]; 
            return sum;
        }
        public static double sum(double[][] d)
        {
            double sum = 0;   
            for (int i = 0; i < d.Length; ++i) 
                for (int j = 0; j < d[i].Length; ++j) 
                    sum += d[i][j];
            return sum;
        }
        public static void TestTime<T, TR>(Func<T, TR> action, T obj, int iterations) 
        { 
            Stopwatch stopwatch = Stopwatch.StartNew();
            for (int i = 0; i < iterations; ++i)      
                action(obj);
            Console.WriteLine(action.Method.Name + " took " + stopwatch.Elapsed);
        }
        public static void TestTime<T1, T2, TR>(Func<T1, T2, TR> action, T1 obj1, T2 obj2, int iterations)
        {
            Stopwatch stopwatch = Stopwatch.StartNew(); 
            for (int i = 0; i < iterations; ++i)    
                action(obj1, obj2); 
            Console.WriteLine(action.Method.Name + " took " + stopwatch.Elapsed);
        }
        public static void Main() {   
            Random random = new Random(); 
            const int l1  = 1024, l2 = 1024; 
            double[ ] d1  = new double[l1 * l2]; 
            double[,] d2  = new double[l1 , l2];  
            double[][] d3 = new double[l1][];   
            for (int i = 0; i < l1; ++i)
            {
                d3[i] = new double[l2];   
                for (int j = 0; j < l2; ++j)  
                    d3[i][j] = d2[i, j] = d1[i * l2 + j] = random.NextDouble();
            }    
            const int iterations = 1000;
            TestTime<double[], int, double>(sum, d1, l1, iterations);
            TestTime<double[,], double>(sum, d2, iterations);

            TestTime<double[][], double>(sum, d3, iterations);
            TestTime<double[], int, double>(sum, d1, l1, iterations);
            TestTime<double[,], double>(sum, d2, iterations);
            TestTime<double[][], double>(sum, d3, iterations); 
        }

    }
}

【讨论】:

  • 我将您的测试程序扩展为 3 维数组 (256*256*256),并得到以下结果(无调试器、发布版本、.Net 4.5、Intel Core 2 Duo @2.20GHz、64 -bit Win7):pastebin.com/SUtMSXkk 如果您有兴趣,这里是程序:pastebin.com/Uzh9jrAM 这表明差异是非常不可忽略的。
  • 是的,这里也一样。我怀疑 OP 选择了“发布”配置,然后使用 F5 运行程序,而不是从命令行运行。或者可能是因为我们运行的是 64 位,也可能是因为 JIT 现在针对这种情况进行了优化..
  • 实际上,看看如果您将数字更改得更高一些会发生什么,例如,如果它运行约 30 秒。我得到了相当显着的结果。例如,多维数组比锯齿状数组慢 2 倍。 (锯齿状数组运行30秒,多维运行1:24秒)
【解决方案3】:

我认为多维比较慢,运行时必须检查两个或更多(三维及以上)边界检查。

【讨论】:

    【解决方案4】:

    因为多维数组只是一种语法糖,因为它实际上只是一个具有一些索引计算魔法的平面数组。另一方面,锯齿状数组就像一个数组数组。使用二维数组,访问一个元素只需要读取一次内存,而使用两级交错数组,则需要读取两次内存。

    编辑: 显然,原始海报将“锯齿状数组”与“多维数组”混为一谈,所以我的推理并不完全正确。对于真正的原因,请查看上面 Jon Skeet 的重炮答案。

    【讨论】:

    • 我很抱歉。我实际上使用的是多维数组,但我使用了错误的术语。对不起!
    • @DrJokepu:使用多维数组应该比使用锯齿数组更快,但实际上恰恰相反。
    • 这个“指数计算魔法”是我问题的核心。它不应该(至少)和我的第一种方法一样快吗?
    【解决方案5】:

    数组边界检查?

    一维数组有一个可以直接访问的长度成员 - 编译时这只是内存读取。

    多维数组需要一个 GetLength(int dimension) 方法调用来处理参数以获取该维度的相关长度。这不会编译为内存读取,因此您会得到方法调用等。

    此外,GetLength(int dimension) 将对参数进行边界检查。

    【讨论】:

    • 嗯,好主意,您是否以某种方式验证了这一点(调试代码、使用反射器等)?
    • 我知道在 Java 中,对 getter 或 setter 的方法调用实际上优化了方法调用并直接访问该值。我不明白为什么 .NET 会有所不同。还将对 GetLength(int index) 的参数进行边界检查。
    • 我很抱歉。我实际上使用的是多维数组,但我使用了错误的术语。对不起!
    • 谢天谢地,我实际上是在谈论多维数组!
    【解决方案6】:

    下界为 0 的一维数组与 IL 中的多维或非 0 下界数组的类型不同(vectorarray IIRC)。 vector 使用起来更简单 - 要获得元素 x,您只需执行 pointer + size * x。对于array,您必须为一维数组执行pointer + size * (x-lower bound),并且为您添加的每个维度执行更多算术运算。

    基本上,CLR 针对更常见的情况进行了优化。

    【讨论】:

    • 我很抱歉。我实际上使用的是多维数组,但我使用了错误的术语。对不起!
    • 我对此感到困惑,多维数组应该比锯齿数组快。如果有的话,那是 CLR 的错。
    • 一个好的编译器应该能够将所有边界检查移到循环前面,并为 d2 生成与 d1 基本相同的代码。这只是证明MS编译器不是很好(对于数组)。
    • @ILoveFortran:JIT 编译器(实际发出或省略检查的地方)针对执行速度进行了高度优化 - 目标是让 JIT 编译比典型的页面错误更快。即便如此,x64 JIT 编译器也完全实现了您所说的优化,而新的编译器(还不是生产版本)RyuJIT 设法获得了更多的优化。而且,事实是,即使是 x86 编译器也删除了如果您使用for (i = 0; i &lt; ar.Length; i++),则完全进行边界检查,因为这样可以保证在 for 循环本身中进行边界检查。
    • @BVernon:我强烈怀疑这是 - 不太专业的数组(即具有更灵活边界和维度的数组)的性能可能会有所提高,但从根本上说,它仍然是一个比 IL 更复杂的场景向量。
    【解决方案7】:

    我和其他人在这里

    我有一个包含三个维度数组的程序,让我告诉你,当我将数组移动到二维时,我看到了巨大的提升,然后我移动到了一维数组。

    最后,我认为我在执行时间上看到了超过 500% 的性能提升。

    唯一的缺点是增加了找出一维数组中的内容的复杂性,而不是三维数组。

    【讨论】:

      【解决方案8】:

      锯齿状数组是类引用的数组(其他数组),直到叶数组,它可能是一个原始类型的数组。因此,为每个其他数组分配的内存可能无处不在。

      而多维数组的内存分配在一个连续的块中。

      【讨论】:

      • 我很抱歉。我实际上使用的是多维数组,但我使用了错误的术语。对不起!
      • 有趣的是,如果你连续分配锯齿状数组,它在内存中也将是连续的,即使它是一堆引用(.NET 托管堆不寻找空闲空间,不像 malloc 等) .这意味着除非您做错了什么,否则您仍然可以很好地使用缓存。
      【解决方案9】:

      边界检查。如果“i”小于 l1,您的“j”变量可能会超过 l2。这在第二个例子中是不合法的

      【讨论】:

      • 我没有投反对票,但不是在这两种情况下都应用了边界检查吗?
      • 边界检查是正确的(或者至少是一个相关方面),但给出的原因是错误的(虽然我没有反对它),只是有更多的边界检查锯齿状数组。 GetLength(int) 在返回相关数组的大小之前检查参数(>0,
      • 我的观点是,在 发布的代码中,使用单维数组模拟多维数组,使用无效索引代码,前提是算术到达边界内的最终索引值
      【解决方案10】:

      我认为,锯齿状数组实际上是数组的数组,因此有两个级别的间接来获取实际数据。

      【讨论】:

      • 我很抱歉。我实际上使用的是多维数组,但我使用了错误的术语。对不起!
      • @Henk:更令人惊讶的是,(IMO)边界检查在 for 循环中使用固定数迭代的多维数组可以被优化掉,因为它的规则(=矩形)性质数组!我猜这个优化并没有因为一些模糊的原因进行。
      猜你喜欢
      • 1970-01-01
      • 2014-03-13
      • 2020-12-16
      • 2014-02-20
      • 2017-05-10
      • 1970-01-01
      • 2016-02-13
      • 2018-11-04
      • 2017-07-03
      相关资源
      最近更新 更多