Каковы различия между многомерным массивом и массивом массивов в C #?

454

Каковы различия между многомерными массивами double[,]и массивом массивовdouble[][] в C #?

Если есть разница, что лучше для каждого?

ecleel
источник
7
Первый double[,]представляет собой прямоугольный массив, в то время double[][]как известен как «зубчатый массив». Первый будет иметь одинаковое количество «столбцов» для каждой строки, а второй (потенциально) будет иметь разное количество «столбцов» для каждой строки.
GreatAndPowerfulOz

Ответы:

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 говорит, что при использовании его для анализа своих проектов следует использовать зубчатые массивы вместо многомерных.

okutane
источник
7
@ Джон, измерь их сам и не делай предположений.
Хосам Али
2
@John: Моя первая реакция тоже, но я ошибся - подробности см. В вопросе Хосамса.
Хенк Холтерман
38
Многомерные массивы логически должны быть более эффективными, но их реализация компилятором JIT - нет. Приведенный выше код бесполезен, так как он не показывает доступ к массиву в цикле.
ILoveFortran
3
@Henk Holterman - см. Мой ответ ниже. Возможно, на окнах массивы работают быстро, но нужно понимать, что это полностью относится к CLR, а не, например, к моно ...
John Leidegren
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 

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

В окнах синхронизация зубчатых массивов значительно выше, примерно так же, как моя собственная интерпретация того, на что должен быть похож внешний вид многомерного массива, см. Single (). К сожалению, 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
Попробуйте рассчитать их самостоятельно, и посмотрите, как они работают. Зубчатые массивы намного лучше оптимизированы в .NET. Это может быть связано с проверкой границ, но независимо от причины, временные параметры и тесты четко показывают, что зубчатые массивы быстрее доступны, чем многомерные.
Хосам Али
10
Но ваше время кажется слишком маленьким (несколько миллисекунд). На этом уровне у вас будет много помех от системных служб и / или драйверов. Сделайте ваши тесты намного больше, по крайней мере, за секунду или две.
Хосам Али
8
@JohnLeidegren: факт, что многомерные массивы работают лучше при индексации одного измерения, чем другого, был понят в течение полувека, поскольку элементы, которые отличаются только одним конкретным измерением, будут последовательно храниться в памяти и во многих типах памяти (в прошлом и присутствует), доступ к последовательным элементам быстрее, чем доступ к удаленным элементам. Я думаю, что в .net нужно добиться оптимальной индексации результатов по последнему индексу, что вы и делали, но тестирование времени с заменой индексов может быть информативным в любом случае.
суперкат
16
@supercat: многомерные массивы в C # хранятся в главном порядке строк , переключение порядка индексов будет медленнее, так как вы будете обращаться к памяти не последовательно. Кстати, указанное время больше не является точным, я получаю почти вдвое более быстрое время для многомерных массивов, чем зазубренные массивы (протестировано на последней версии .NET CLR), как и должно быть ..
Amro
9
Я знаю, что это немного педантично, но я должен отметить, что это не Windows против Mono, а CLR против Mono. Вы, кажется, иногда путаете их. Два не эквивалентны; Mono работает и на Windows.
Магус
70

Проще говоря, многомерные массивы похожи на таблицы в СУБД.
Array of Array (jagged array) позволяет каждому элементу содержать другой массив того же типа переменной длины.

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

Например, Psuedocode:

int[,] data = new int[2,2];
data[0,0] = 1;
data[0,1] = 2;
data[1,0] = 3;
data[1,1] = 4;

Думайте об этом как о таблице 2х2:

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
shahkalpesh
источник
4
это то, что действительно имеет значение при принятии решения о том, что использовать .. не эта скорость чтоли .. ну скорость может стать фактором, когда у вас есть квадратный массив.
Xaser
46

Предисловие: Этот комментарий предназначен для ответа на вопрос, предоставленный okutane , но из-за глупой системы репутации SO, я не могу опубликовать его там, где он принадлежит.

Ваше утверждение, что один медленнее другого из-за вызовов метода, неверно. Один медленнее другого из-за более сложных алгоритмов проверки границ. В этом легко убедиться, посмотрев не на IL, а на скомпилированную сборку. Например, в моей версии 4.5 доступ к элементу (через указатель в edx), хранящемуся в двумерном массиве, на который указывает ecx, с индексами, хранящимися в 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). Это вычисление примерно такое же быстрое (одно умножение может быть заменено сдвигом, так как это единственная причина, по которой мы выбираем байты для измерения в виде степеней двух бит), как и все остальное для произвольного доступа к элементу.

Другая сложность заключается в том, что существует множество случаев, когда современный компилятор оптимизирует удаленную проверку границ для доступа к элементам при выполнении итерации по одномерному массиву. В результате получается код, который в основном просто перемещает указатель индекса по непрерывной памяти массива. Наивная итерация по многомерным массивам обычно включает дополнительный уровень вложенной логики, поэтому компилятор с меньшей вероятностью оптимизирует работу. Таким образом, даже несмотря на то, что накладные расходы на проверку границ при доступе к одному элементу амортизируются до постоянного времени выполнения в отношении размеров и размеров массива, простой тестовый пример для измерения разницы может выполняться во много раз дольше для выполнения.

Eglin
источник
1
Спасибо за исправление ответа окутане (не Дмитрия). Раздражает, что люди дают неправильные ответы на Stackoverflow и получают 250 голосов за, в то время как другие дают правильные ответы и получают гораздо меньше. Но в конце концов код IL не имеет значения. Вы должны действительно ИЗМЕРЯТЬ скорость, чтобы что-то сказать о производительности. Ты сделал это? Я думаю, что разница будет нелепой.
Эльмуэ
38

Я хотел бы обновить это, потому что в .NET Core многомерные массивы работают быстрее, чем зубчатые массивы . Я выполнил тесты от Джона Лейдгрена, и это результаты предварительного просмотра .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 инструкции для выполнения

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

adsamcik
источник
14

Многомерные массивы - это (n-1) -мерные матрицы.

Так int[,] square = new int[2,2]же квадратная матрица 2х2,int[,,] cube = new int [3,3,3] это куб - квадратная матрица 3х3. Пропорциональность не требуется.

Зубчатые массивы - это просто массив массивов - массив, в котором каждая ячейка содержит массив.

Так что MDA пропорциональны, JD может и не быть! Каждая ячейка может содержать массив произвольной длины!

abatishchev
источник
7

Это могло быть упомянуто в ответах выше, но не в явном виде: с помощью зубчатого массива вы можете array[row]ссылаться на целый ряд данных, но это не разрешено для многомерных массивов.

lznt
источник
4

В дополнение к другим ответам, обратите внимание, что многомерный массив выделяется как один большой коренастый объект в куче. Это имеет некоторые последствия:

  1. Некоторые многомерные массивы будут размещены в куче больших объектов (LOH), где в противном случае их эквивалентные зубчатые массивы не имели бы.
  2. GC потребуется найти один непрерывный свободный блок памяти для выделения многомерного массива, тогда как зубчатый массив может заполнить пробелы, вызванные фрагментацией кучи ... это обычно не проблема в .NET из-за сжатия , но LOH не уплотняется по умолчанию (вы должны просить об этом, и вы должны спрашивать каждый раз, когда захотите).
  3. Вы хотите , чтобы посмотреть в <gcAllowVeryLargeObjects>для многомерных массивов , как до того , как проблема будет когда - либо, если вы только когда - либо использовать неровные массивы.
Джо Амента
источник
2

Я разбираю файлы .il, сгенерированные ildasm, для создания базы данных сборок, классов, методов и хранимых процедур для использования при преобразовании. Я наткнулся на следующее, что сломало мой разбор.

.method private hidebysig instance uint32[0...,0...] 
        GenerateWorkingKey(uint8[] key,
                           bool forEncryption) cil managed

В книге поясняется книга Expert .NET 2.0 IL Assembler, написанная Сергеем Лидином, Apress, 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 и неопределенного размера.

Том

Томас Хатчинсон
источник