性能-为什么.NET中的多维数组比普通数组慢?

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

我正在尝试使用锯齿状今天的多维数组,当我注意到它的性能不如我预期的那样时。 与使用二维数组相比,使用一维数组和手动计算索引要快得多(几乎两倍)。 我使用TestTime数组(初始化为随机值)进行了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 vs52。)这主要是由于对TestTime的调用。编译器还将对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);
}
Hosam Aly asked 2020-02-12T22:05:30Z
9个解决方案
44 votes

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

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

Jon Skeet answered 2020-02-12T22:05:42Z
10 votes

数组边界检查?

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

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

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

JeeBee answered 2020-02-12T22:06:16Z
4 votes

有趣的是,我从上面运行了以下代码在Vista盒子上使用VS2008 NET3.5SP1 Win32,在发布/优化中,差异几乎无法测量,而调试/否定多维度阵列要慢得多。(我两次运行了三个测试,以减少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 / unoptimize中,多维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); 
        }

    }
}
Cameron answered 2020-02-12T22:06:50Z
3 votes

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

编辑:显然,原始海报将“锯齿状阵列”与“多维阵列”混合在一起,因此我的推理并不完全正确。 出于真正原因,请检查上述乔恩·斯基特(Jon Skeet)的重型炮兵答案。

Tamas Czinege answered 2020-02-12T22:07:15Z
2 votes

锯齿状数组是类引用的数组(其他数组),直到叶数组(可能是原始类型的数组)为止。 因此,为其他每个阵列分配的内存都可以放置在各处。

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

AnthonyWJones answered 2020-02-12T22:07:40Z
1 votes

我认为这与锯齿状的数组实际上是数组的数组有关,因此有两种方式可以获取实际数据。

Autodidact answered 2020-02-12T22:08:00Z
1 votes

我和这里的其他人在一起

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

最后,我认为执行时间使性能提高了500%以上。

唯一的缺点是添加复杂性以找出一维数组相对于三个维的位置。

Fredou answered 2020-02-12T22:08:33Z
1 votes

我认为多维速度较慢,运行时必须检查两个或多个(三维和向上)边界检查。

Michael Buen answered 2020-02-12T22:08:53Z
-1 votes

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

Damien_The_Unbeliever answered 2020-02-12T22:09:14Z
translate from https://stackoverflow.com:/questions/468832/why-are-multi-dimensional-arrays-in-net-slower-than-normal-arrays