Quali sono le differenze tra una matrice multidimensionale e una matrice di matrici in C #?


454

Quali sono le differenze tra array multidimensionali double[,]e array di array double[][]in C #?

Se c'è una differenza, qual è l'uso migliore per ognuno?


7
Il primo double[,]è un array rettangolare, mentre double[][]è noto come un "array frastagliato". Il primo avrà lo stesso numero di "colonne" per ogni riga, mentre il secondo avrà (potenzialmente) un numero diverso di "colonne" per ogni riga.
GreatAndPowerfulOz,

Risposte:


334

Le matrici di matrici (matrici frastagliate) sono più veloci delle matrici multidimensionali e possono essere utilizzate in modo più efficace. Le matrici multidimensionali hanno una sintassi migliore.

Se si scrive un codice semplice utilizzando matrici frastagliate e multidimensionali e quindi si ispeziona l'assieme compilato con un disassemblatore IL, si vedrà che la memorizzazione e il recupero da matrici frastagliate (o monodimensionali) sono semplici istruzioni IL mentre le stesse operazioni per matrici multidimensionali sono metodo invocazioni che sono sempre più lente.

Considera i seguenti metodi:

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 loro IL sarà il seguente:

.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

Quando si utilizzano array frastagliati, è possibile eseguire facilmente operazioni come lo scambio di righe e il ridimensionamento delle righe. Forse in alcuni casi l'uso di array multidimensionali sarà più sicuro, ma anche Microsoft FxCop dice che gli array frastagliati dovrebbero essere usati anziché multidimensionali quando lo usi per analizzare i tuoi progetti.


7
@Giovanni, misurali tu stesso e non fare ipotesi.
Hosam Aly,

2
@John: Anche la mia prima reazione, ma ho sbagliato - vedi la domanda di Hosams per i dettagli.
Henk Holterman,

38
Le matrici multidimensionali dovrebbero logicamente essere più efficaci, ma la loro implementazione da parte del compilatore JIT non lo è. Il codice sopra riportato non è utile poiché non mostra l'accesso alla matrice in un ciclo.
ILoveFortran,

3
@Henk Holterman - Vedi la mia risposta di seguito, potrebbe essere il caso che su Windows le matrici frastagliate siano veloci ma bisogna rendersi conto che questo è interamente specifico per CLR e non è il caso con ad esempio mono ...
John Leidegren

12
So che questa è una vecchia domanda, mi chiedo solo se il CLR è stato ottimizzato per array multidimensionali da quando è stata posta questa domanda.
Anthony Nichols,

197

Un array multidimensionale crea un piacevole layout di memoria lineare mentre un array frastagliato implica diversi livelli extra di indiretta.

Cercare il valore jagged[3][6]in un array frastagliato var jagged = new int[10][5]funziona in questo modo: Cerca l'elemento nell'indice 3 (che è un array) e cerca l'elemento nell'indice 6 in tale array (che è un valore). Per ogni dimensione in questo caso, c'è un'ulteriore ricerca (questo è un modello di accesso alla memoria costoso).

Un array multidimensionale è disposto linearmente in memoria, il valore effettivo si trova moltiplicando gli indici. Tuttavia, dato l'array var mult = new int[10,30], la Lengthproprietà di quell'array multidimensionale restituisce il numero totale di elementi, ovvero 10 * 30 = 300.

La Rankproprietà di un array frastagliato è sempre 1, ma un array multidimensionale può avere qualsiasi rango. Il GetLengthmetodo di qualsiasi array può essere utilizzato per ottenere la lunghezza di ogni dimensione. Per l'array multidimensionale in questo esempio mult.GetLength(1)restituisce 30.

L'indicizzazione dell'array multidimensionale è più veloce. ad esempio, dato l'array multidimensionale in questo esempio mult[1,7]= 30 * 1 + 7 = 37, ottenere l'elemento in quell'indice 37. Questo è un modello di accesso alla memoria migliore perché è coinvolta solo una posizione di memoria, che è l'indirizzo di base dell'array.

Un array multidimensionale alloca quindi un blocco di memoria continuo, mentre un array frastagliato non deve essere quadrato, ad es. jagged[1].LengthNon deve essere uguale jagged[2].Length, il che sarebbe vero per qualsiasi array multidimensionale.

Prestazione

Le matrici multidimensionali per quanto riguarda le prestazioni dovrebbero essere più veloci. Molto più veloce, ma a causa di una cattiva implementazione del CLR non lo sono.

 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 

La prima riga indica i tempi delle matrici frastagliate, la seconda mostra le matrici multidimensionali e la terza, ecco come dovrebbe essere. Il programma è mostrato di seguito, FYI questo è stato testato in esecuzione mono. (I tempi di Windows sono molto diversi, principalmente a causa delle variazioni di implementazione CLR).

Su Windows, i tempi delle matrici frastagliate sono notevolmente superiori, più o meno come la mia interpretazione di come dovrebbe essere la matrice multidimensionale, vedi 'Single ()'. Purtroppo il compilatore JIT di Windows è davvero stupido, e questo purtroppo rende difficili queste discussioni sulle prestazioni, ci sono troppe incongruenze.

Questi sono i tempi che ho ottenuto su Windows, lo stesso affare qui, la prima riga sono matrici frastagliate, la seconda multidimensionale e la terza la mia implementazione del multidimensionale, nota quanto questo è più lento su Windows rispetto al mono.

  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

Codice sorgente:

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
Prova a cronometrarli da solo e guarda come si comportano entrambi. Le matrici frastagliate sono molto più ottimizzate in .NET. Può essere correlato al controllo dei limiti, ma indipendentemente dal motivo, i tempi e i benchmark mostrano chiaramente che gli array frastagliati sono più veloci di accesso rispetto a quelli multidimensionali.
Hosam Aly,

10
Ma i tuoi tempi sembrano essere troppo piccoli (pochi millisecondi). A questo livello avrai molte interferenze dai servizi di sistema e / o dai driver. Rendi i tuoi test molto più grandi, almeno impiegando un secondo o due.
Hosam Aly,

8
@JohnLeidegren: Il fatto che gli array multidimensionali funzionino meglio quando si indicizza una dimensione rispetto a un'altra è stato compreso per mezzo secolo, poiché gli elementi che differiscono in una sola dimensione particolare verranno archiviati consecutivamente nella memoria e con molti tipi di memoria (passato e presente), accedere ad elementi consecutivi è più veloce dell'accesso ad oggetti distanti. Penso che in .net si dovrebbe ottenere l'indicizzazione dei risultati ottimali dall'ultimo pedice che è quello che stavi facendo, ma testare il tempo con i pedici scambiati può essere comunque informativo.
Supercat,

16
@supercat: le matrici multidimensionali in C # sono archiviate nell'ordine delle righe principali , scambiando l'ordine degli indici sarebbe più lento poiché si accederebbe alla memoria in modo non consecutivo. A proposito i tempi riportati non sono più precisi, ottengo tempi quasi due volte più veloci per gli array multidimensionali rispetto agli array frastagliati (testati sull'ultimo .NET CLR), che è come dovrebbe essere ..
Amro

9
So che questo è un po 'pedante, ma devo dire che questo non è Windows vs Mono, ma CLR vs Mono. A volte sembra confondere quelli. I due non sono equivalenti; Mono funziona anche su Windows.
Magus,

70

In poche parole, gli array multidimensionali sono simili a una tabella in DBMS.
Array of Array (array frastagliato) consente a ciascun elemento di contenere un altro array dello stesso tipo di lunghezza variabile.

Pertanto, se si è certi che la struttura dei dati assomigli a una tabella (righe / colonne fisse), è possibile utilizzare un array multidimensionale. Gli array frastagliati sono elementi fissi e ogni elemento può contenere un array di lunghezza variabile

Ad esempio Psuedocode:

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

Pensa a quanto sopra come una tabella 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 }; 

Pensa a quanto sopra come ogni riga con un numero variabile di colonne:

 1 |  2 |  3 | 4
11 | 12
21 | 22 | 23

4
questo è ciò che conta davvero quando si decide cosa usare .. non questa cosa della velocità ... beh, la velocità può diventare un fattore quando si ha un array quadrato.
Xaser,

46

Prefazione: questo commento ha lo scopo di rispondere alla risposta fornita da okutane , ma a causa del sistema di reputazione sciocco di SO, non posso pubblicarlo dove appartiene.

La tua affermazione che uno è più lento dell'altro a causa delle chiamate del metodo non è corretta. Uno è più lento dell'altro a causa di algoritmi di controllo dei limiti più complicati. È possibile verificarlo facilmente osservando non IL, ma l'assembly compilato. Ad esempio, sulla mia installazione 4.5, l'accesso a un elemento (tramite puntatore in edx) memorizzato in un array bidimensionale a cui punta ecx con indici memorizzati in eax ed edx è simile al seguente:

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]

Qui puoi vedere che non ci sono spese generali per le chiamate al metodo. Il controllo dei limiti è molto complicato grazie alla possibilità di indici diversi da zero, che è una funzionalità non disponibile con array frastagliati. Se rimuoviamo sub, cmp e jmps per i casi diversi da zero, il codice praticamente si risolve (x*y_max+y)*sizeof(ptr)+sizeof(array_header). Questo calcolo è più veloce (una moltiplicazione potrebbe essere sostituita da uno spostamento, poiché questa è l'intera ragione per cui scegliamo byte da dimensionare come potenze di due bit) come qualsiasi altra cosa per l'accesso casuale a un elemento.

Un'altra complicazione è che ci sono molti casi in cui un moderno compilatore ottimizzerà il controllo dei limiti nidificati per l'accesso agli elementi durante l'iterazione su un array a dimensione singola. Il risultato è un codice che sostanzialmente fa avanzare un puntatore indice sulla memoria contigua dell'array. L'iterazione ingenua su array multidimensionali comporta generalmente un ulteriore livello di logica nidificata, quindi un compilatore ha meno probabilità di ottimizzare l'operazione. Quindi, anche se il sovraccarico di controllo dei limiti dell'accesso a un singolo elemento si ammortizza in un runtime costante rispetto alle dimensioni e alle dimensioni dell'array, un semplice test-case per misurare la differenza può richiedere molte volte più tempo per essere eseguito.


1
Grazie per aver corretto la risposta di okutane (non Dmitry). È fastidioso che le persone diano risposte sbagliate su StackOverflow e ottengano 250 voti positivi, mentre gli altri danno risposte corrette e ottengono molto meno. Ma alla fine il codice IL è irrilevante. Devi davvero MISURARE la velocità per dire qualcosa sulle prestazioni. Sei stato tu? Penso che la differenza sarà ridicola.
Elmue,

38

Vorrei aggiornarlo, perché in .NET Core gli array multidimensionali sono più veloci degli array frastagliati . Ho eseguito i test di John Leidegren e questi sono i risultati dell'anteprima di .NET Core 2.0 2. Ho aumentato il valore della dimensione per rendere meno visibili eventuali influenze dalle app in background.

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 

Ho esaminato gli smontaggi e questo è quello che ho trovato

jagged[i][j][k] = i * j * k; necessarie 34 istruzioni per l'esecuzione

multi[i, j, k] = i * j * k; necessarie 11 istruzioni per l'esecuzione

single[i * dim * dim + j * dim + k] = i * j * k; necessarie 23 istruzioni per l'esecuzione

Non sono riuscito a identificare il motivo per cui gli array monodimensionali erano ancora più veloci di quelli multidimensionali, ma la mia ipotesi è che abbia a che fare con un po 'di ottimizzazione effettuata sulla CPU


14

Le matrici multidimensionali sono matrici di dimensioni (n-1).

Così int[,] square = new int[2,2]è matrice quadrata 2x2, int[,,] cube = new int [3,3,3]è un cubo - matrice quadrata 3x3. La proporzionalità non è richiesta.

Gli array frastagliati sono solo array di array, un array in cui ogni cella contiene un array.

Quindi gli MDA sono proporzionali, JD potrebbe non esserlo! Ogni cella può contenere una matrice di lunghezza arbitraria!


7

Ciò potrebbe essere stato menzionato nelle risposte precedenti ma non esplicitamente: con l'array frastagliato è possibile utilizzare array[row]per fare riferimento a un'intera riga di dati, ma ciò non è consentito per gli array multi-d.


4

Oltre alle altre risposte, notare che un array multidimensionale è allocato come un grosso grosso oggetto sull'heap. Ciò ha alcune implicazioni:

  1. Alcuni array multidimensionali verranno allocati sul Large Object Heap (LOH) dove altrimenti non avrebbero le loro controparti array array frastagliate equivalenti.
  2. Il GC dovrà trovare un singolo blocco di memoria libero contiguo per allocare un array multidimensionale, mentre un array frastagliato potrebbe essere in grado di colmare i vuoti causati dalla frammentazione dell'heap ... questo non è di solito un problema in .NET a causa della compattazione , ma LOH non viene compattato per impostazione predefinita (devi richiederlo e devi chiederlo ogni volta che lo desideri).
  3. Ti consigliamo di cercare <gcAllowVeryLargeObjects>array multidimensionali molto prima che il problema si presenti se usi sempre array di frastagliati.

2

Sto analizzando i file .il generati da ildasm per creare un database di assunzioni, classi, metodi e procedure memorizzate per l'uso durante la conversione. Mi sono imbattuto nel seguente, che ha interrotto la mia analisi.

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

Il libro Expert .NET 2.0 IL Assembler, di Serge Lidin, Apress, pubblicato nel 2006, capitolo 8, Tipi e firme primitivi, pagg. 149-150 spiega.

<type>[]è definito un vettore di <type>,

<type>[<bounds> [<bounds>**] ] è definito un array di <type>

**i mezzi possono essere ripetuti, i [ ]mezzi facoltativi.

Esempi: Let <type> = int32.

1) int32[...,...]è una matrice bidimensionale di limiti e dimensioni inferiori indefiniti

2) int32[2...5]è una matrice unidimensionale di limite inferiore 2 e dimensione 4.

3) int32[0...,0...]è una matrice bidimensionale con limiti inferiori 0 e dimensioni indefinite.

Tom

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.