Які відмінності між багатовимірним масивом та масивом масивів у C #?


454

Які відмінності між багатовимірними масивами double[,]та масивами масивів double[][]у C #?

Якщо є різниця, що найкраще використовувати для кожного?


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 каже, що нерівні масиви слід використовувати замість багатовимірних, коли ви використовуєте його для аналізу своїх проектів.


7
@John, вимірюй їх самостійно, і не роби припущень.
Hosam Aly

2
@John: Моя перша реакція теж, але я помилився - детальніше див. Питання Хоссам.
Хенк Холтерман

38
Багатовимірні масиви логічно повинні бути більш ефективними, але їх реалізація компілятором JIT не є. Вищевказаний код не корисний, оскільки не показує доступ до масиву в циклі.
ILoveFortran

3
@Henk Holterman - Дивіться мою відповідь нижче: Можливо, у випадку, коли на зубчастих масивах Windows швидкі, але треба усвідомлювати, що це цілком CLR, а не це стосується, наприклад, моно ...
Джон Лейдегрен

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 

Перший ряд - це синхронізація нерівних масивів, другий показує багатовимірні масиви, а третій, ну ось яким він повинен бути. Програма показана нижче, FYI це тестували під керуванням моно. (Терміни роботи вікон сильно відрізняються, в основному через зміни в реалізації CLR).

У вікнах синхронізування зубчастих масивів сильно перевершує, приблизно таке ж, як і моя інтерпретація того, яким має виглядати багатовимірний масив, див. "Один ()". На жаль, Windows JIT-компілятор справді дурний, і це, на жаль, ускладнює ці дискусії щодо продуктивності, є занадто багато невідповідностей.

Це терміни, які я отримав на windows, тут же справа, перший рядок - це зубчасті масиви, другий багатовимірний і третій - моя власна реалізація багатовимірності, зауважте, наскільки повільніше це на 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 слід отримати оптимальну індексацію результатів за останнім індексом, що саме ви робили, але тестування часу з обміненими підписками може бути інформативним у будь-якому випадку.
supercat

16
@supercat: багатовимірні масиви в C # зберігаються в основному рядку , змінюючи порядок підписок буде повільніше, оскільки ви будете отримувати доступ до пам'яті не послідовно. До речі, повідомлені часи більше не є точними, я отримую майже вдвічі швидший час для багатовимірних масивів, ніж зубчасті масиви (протестовано на останньому .NET CLR), як це повинно бути ..
Amro

9
Я знаю, що це трохи педантично, але мушу зазначити, що це не Windows проти Mono, а CLR vs Mono. Ви, здається, іноді їх плутаєте. Два не рівнозначні; Моно працює і в Windows.
Маг

70

Простіше кажучи, багатовимірні масиви схожі на таблицю в СУБД.
Масив масиву (нерівний масив) дозволяє кожному елементу утримувати інший масив того ж типу змінної довжини.

Отже, якщо ви впевнені, що структура даних виглядає як таблиця (фіксовані рядки / стовпці), ви можете використовувати багатовимірний масив. Нерівний масив - це нерухомі елементи, і кожен елемент може містити масив змінної довжини

Наприклад, Psuedocode:

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

4
це те, що насправді має значення при вирішенні питання, що використовувати .. не ця швидкість, а швидкість може стати фактором, коли у вас є квадратний масив.
Ксасер

46

Передмова: Цей коментар призначений для вирішення відповіді, окутаної , але через дурну систему репутації SO я не можу розміщувати його там, де належить.

Твоє твердження, що одна повільніше, ніж інша через виклики методу, невірна. Одне відбувається повільніше, ніж інше через складніші алгоритми перевірки меж. Ви можете легко перевірити це, дивлячись не на ІЛ, а на складену збірку. Наприклад, на моїй установці 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). Цей обчислення приблизно такий же швидкий (один множник може бути замінений зсувом, оскільки це вся причина, через яку ми обираємо байти розміром як потужність двох біт), як і все інше для випадкового доступу до елемента.

Ще одне ускладнення полягає в тому, що існує маса випадків, коли сучасний компілятор оптимізує перевірку вкладених меж для доступу до елементів під час ітерації над одновимірним масивом. Результатом є код, який в основному просто просуває покажчик покажчика на суміжну пам'ять масиву. Наївна ітерація над багатовимірними масивами, як правило, включає додатковий шар вкладеної логіки, тому компілятор має меншу ймовірність оптимізувати операцію. Таким чином, навіть незважаючи на те, що перевірка меж доступу до одного елемента амортизується на постійний час виконання щодо розмірів і розмірів масиву, простий тестовий випадок для вимірювання різниці може зайняти багато разів більше часу.


1
Дякую за виправлення відповіді окутане (не Дмитра). Прикро, що люди дають неправильні відповіді на Stackoverflow і отримують 250 голосів, а інші дають правильні відповіді та отримують набагато менше. Але врешті-решт код ІЛ не має значення. Ви повинні дійсно ВИМІРИТИ швидкість, щоб сказати що-небудь про продуктивність. Ти це робив? Я думаю, що різниця буде смішною.
Елмуе

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 інструкції

Я не зміг визначити, чому одновимірні масиви все ще швидші, ніж багатовимірні, але я здогадуюсь, що це стосується певної оптимізації, зробленої на процесорі


14

Багатовимірні масиви є матрицями розмірів (n-1).

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

Нерівні масиви - це просто масив масивів - масив, де кожна комірка містить масив.

Тож MDA пропорційні, JD може і не бути! Кожна комірка може містити масив довільної довжини!


7

Про це, можливо, було сказано у вищезазначених відповідях, але не явно: з нерівним масивом ви можете використовувати array[row]для посилання на цілий ряд даних, але це не дозволяється для багатодискових масивів.


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

Книга Експерт .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 та невизначеного розміру.

Том

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.