Почему многомерные массивы в .NET медленнее, чем обычные массивы? - PullRequest
47 голосов
/ 22 января 2009

Редактировать: Я извиняюсь перед всеми. Я использовал термин «зубчатый массив», когда на самом деле хотел сказать «многомерный массив» (как видно из моего примера ниже). Я прошу прощения за использование неправильного имени. На самом деле я обнаружил, что зубчатые массивы быстрее, чем многомерные! Я добавил свои измерения для зубчатых массивов.

Я пытался использовать многомерный массив с зазубринами сегодня, когда заметил, что его производительность не соответствует ожиданиям. Использование одномерного массива и ручное вычисление индексов было намного быстрее (почти в два раза), чем использование двумерного массива. Я написал тест, используя массивы 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). Компилятор также генерирует вызовы Array::Get для многомерного массива jagged , тогда как для простого массива он просто вызывает ldelem.

Так что мне интересно, почему доступ через многомерные массивы медленнее, чем обычные массивы? Я бы предположил, что компилятор (или JIT) будет делать нечто похожее на то, что я делал в моем первом методе, но на самом деле это не так.

Не могли бы вы помочь мне понять, почему это происходит именно так?


Обновление: В соответствии с предложением Хенка Холтермана, вот реализация 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);
}

Ответы [ 9 ]

42 голосов
/ 22 января 2009

Одномерные массивы с нижней границей 0 относятся к многомерным или ненулевым массивам с нижней границей в пределах IL (vector против array IIRC). С vector работать проще - чтобы добраться до элемента х, достаточно просто pointer + size * x. Для array вы должны сделать pointer + size * (x-lower bound) для одномерного массива, и еще больше арифметики для каждого добавляемого измерения.

По сути, CLR оптимизирован для гораздо более распространенного случая.

9 голосов
/ 22 января 2009

Проверка границ массива?

В одномерном массиве есть элемент длины, к которому вы обращаетесь напрямую - при компиляции это просто чтение из памяти.

Для многомерного массива требуется вызов метода GetLength (int dimension), который обрабатывает аргумент для получения соответствующей длины для этого измерения. Это не компилируется до чтения из памяти, поэтому вы получаете вызов метода и т. Д.

Кроме того, GetLength (размерность int) будет выполнять проверку границ параметра.

4 голосов
/ 29 июня 2009

Интересно, я запустил следующий код сверху используя VS2008 NET3.5SP1 Win32 на коробке Vista, и в выпуске / оптимизации разница была едва заметна, в то время как отладка / 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 многомерность против одиночный / зубчатый имеет огромное значение.

Полная программа:

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 голосов
/ 22 января 2009

Потому что многомерный массив - это просто синтаксический сахар, так как на самом деле это просто плоский массив с некоторой магией вычисления индекса. С другой стороны, зубчатый массив подобен массиву массивов. При использовании двумерного массива для доступа к элементу требуется чтение памяти только один раз, а при использовании двухуровневого массива с неровностями необходимо дважды прочитать память.

РЕДАКТИРОВАТЬ: Очевидно, что оригинальный плакат смешал "зубчатые массивы" с "многомерными массивами", поэтому мои рассуждения не совсем верны. По реальной причине, проверьте ответ тяжелой артиллерии Джона Скита выше.

2 голосов
/ 22 января 2009

Зубчатые массивы - это массивы ссылок на классы (другие массивы) до конечного массива, который может быть массивом примитивного типа. Следовательно, память, выделенная для каждого из других массивов, может быть повсюду.

Принимая во внимание, что многомерный массив имеет память, выделенную в одном смежном кусочке.

1 голос
/ 22 января 2009

Я думаю, что многомерность медленнее, среда выполнения должна проверять проверку двух или более (трехмерных и более) границ.

1 голос
/ 22 января 2009

Я со всеми здесь

У меня была программа с трехмерным массивом, позвольте мне сказать вам, что когда я переместил массив в два измерения, я увидел огромный прирост, а затем перешел на одномерный массив.

В итоге, я думаю, что во время выполнения я увидел увеличение производительности более чем на 500%.

Единственным недостатком была сложность, позволяющая выяснить, что было в одномерном массиве по сравнению с трехмерным.

1 голос
/ 22 января 2009

Я думаю, что это имеет какое-то отношение к тому факту, что зубчатые массивы на самом деле являются массивами массивов, следовательно, существует два уровня косвенности, чтобы добраться до фактических данных.

0 голосов
/ 22 января 2009

Проверка границ. Ваша переменная "j" может превышать l2, если "i" меньше l1. Это не будет законным во втором примере

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...