Quelles sont les différences entre les tableaux multidimensionnels double[,]
et les tableaux de tableaux double[][]
en C #?
S'il y a une différence, quelle est la meilleure utilisation pour chacun?
Les tableaux de tableaux (tableaux dentelés) sont plus rapides que les tableaux multidimensionnels et peuvent être utilisés plus efficacement. Les tableaux multidimensionnels ont une syntaxe plus agréable.
Si vous écrivez du code simple à l'aide de tableaux multidimensionnels et déchiquetés, puis inspectez l'assembly compilé à l'aide d'un désassembleur IL, vous constaterez que le stockage et la récupération à partir de tableaux déchiquetés (ou à une dimension) sont de simples instructions IL, tandis que les mêmes opérations pour les tableaux multidimensionnels sont effectuées. invocations qui sont toujours plus lentes.
Considérez les méthodes suivantes:
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;
}
Leur IL sera le suivant:
.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
Lorsque vous utilisez des tableaux dentelés, vous pouvez facilement effectuer des opérations telles que l’échange et le redimensionnement de lignes. Peut-être que dans certains cas, l’utilisation de tableaux multidimensionnels sera plus sûre, mais même Microsoft FxCop indique que les tableaux déchiquetés doivent être utilisés au lieu de multidimensionnels lorsque vous l’utilisez pour analyser vos projets.
Un tableau multidimensionnel crée une structure de mémoire linéaire Nice alors qu'un tableau en dents de scie implique plusieurs niveaux d'indirection supplémentaires.
La recherche de la valeur jagged[3][6]
dans un tableau irrégulier var jagged = new int[10][5]
fonctionne comme suit: Recherchez l'élément à l'index 3 (qui est un tableau) et recherchez l'élément à l'index 6 dans ce tableau (qui est un valeur). Pour chaque dimension dans ce cas, il existe une recherche supplémentaire (il s'agit d'un modèle d'accès mémoire coûteux).
Un tableau multidimensionnel est structuré de manière linéaire en mémoire, la valeur réelle est trouvée en multipliant les index. Cependant, étant donné le tableau var mult = new int[10,30]
, la propriété Length
de ce tableau multidimensionnel renvoie le nombre total d’éléments, à savoir 10 * 30 = 300.
La propriété Rank
d'un tableau en escalier est toujours égale à 1, mais un tableau multidimensionnel peut avoir n'importe quel rang. La méthode GetLength
de n'importe quel tableau peut être utilisée pour obtenir la longueur de chaque dimension. Dans cet exemple, le tableau multidimensionnel mult.GetLength(1)
renvoie 30.
L'indexation du tableau multidimensionnel est plus rapide. par exemple. étant donné le tableau multidimensionnel de cet exemple mult[1,7]
= 30 * 1 + 7 = 37, obtenez l'élément à cet index 37. Il s'agit d'un meilleur modèle d'accès à la mémoire car un seul emplacement de mémoire est impliqué, qui est l'adresse de base de le tableau.
Un tableau multidimensionnel alloue donc un bloc de mémoire continu, alors qu'un tableau en dents de scie n'a pas besoin d'être carré, par ex. jagged[1].Length
ne doit pas nécessairement être égal à jagged[2].Length
, ce qui serait vrai pour tout tableau multidimensionnel.
En termes de performances, les tableaux multidimensionnels devraient être plus rapides. Beaucoup plus rapidement, mais à cause d'une très mauvaise implémentation CLR, ils ne le sont pas.
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 première ligne contient les timings des tableaux en dents de scie, la seconde affiche les tableaux multidimensionnels et la troisième, c'est bien ainsi. Le programme est présenté ci-dessous, pour votre information, cela a été testé en mode mono. (Les horaires de Windows sont très différents, principalement en raison des variations de la mise en œuvre du CLR).
Sur les fenêtres, les timings des tableaux déchiquetés sont nettement supérieurs, à peu près identiques à ceux de ma propre interprétation de ce à quoi devrait ressembler un tableau multidimensionnel, voir 'Single ()'. Malheureusement, le compilateur JIT de Windows est vraiment stupide, et cela rend malheureusement ces discussions sur les performances difficiles, car il y a trop d'incohérences.
Ce sont les timings que j'ai sur les fenêtres, même affaire ici, la première rangée sont des tableaux déchiquetés, la deuxième multidimensionnelle et la troisième ma propre implémentation de multidimensionnelle, notez combien cela est plus lent sur les fenêtres par rapport à 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
Code source:
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();
}
}
Les tableaux multidimensionnels sont simplement similaires à une table dans un SGBD.
Tableau de tableau (tableau irrégulier) permet à chaque élément de contenir un autre tableau du même type de longueur variable.
Ainsi, si vous êtes certain que la structure des données ressemble à une table (lignes/colonnes fixes), vous pouvez utiliser un tableau multidimensionnel. Les tableaux en dents de scie sont des éléments fixes et chaque élément peut contenir un tableau de longueur variable
Par exemple. Psuedocode:
int[,] data = new int[2,2];
data[0,0] = 1;
data[0,1] = 2;
data[1,0] = 3;
data[1,1] = 4;
Pensez à ce qui précède comme une table 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 };
Pensez à ce qui précède comme chaque ligne ayant un nombre variable de colonnes:
1 | 2 | 3 | 4 11 | 12 21 | 22 | 23
Préface: Ce commentaire est destiné à adresser la réponse fournie par okutane , mais à cause du système de réputation stupide de SO, je ne peux pas postez-le où il appartient.
Votre affirmation selon laquelle l'un est plus lent que l'autre à cause des appels de méthode n'est pas correcte. L’un est plus lent que l’autre en raison de la complexité des algorithmes de vérification des limites. Vous pouvez facilement le vérifier en regardant non pas l’IL, mais l’Assemblée compilée. Par exemple, sur mon installation 4.5, accéder à un élément (via un pointeur dans edx) stocké dans un tableau à deux dimensions pointé par ecx avec des index stockés dans eax et edx ressemble à ceci:
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]
Ici, vous pouvez voir qu’il n’ya pas de surcharge liée aux appels de méthode. La vérification des limites est juste très compliquée grâce à la possibilité d’index non nuls, une fonctionnalité qui n’est pas proposée avec les tableaux déchiquetés. Si nous supprimons les sous, cmp et jmps pour les cas non nuls, le code se résorbera plutôt en (x*y_max+y)*sizeof(ptr)+sizeof(array_header)
. Ce calcul est à peu près aussi rapide (une multiplication pourrait être remplacée par un décalage, car c’est la raison pour laquelle nous avons choisi la taille en octets d'une puissance de deux bits), comme toute autre chose pour un accès aléatoire à un élément.
Une autre complication réside dans le fait qu’il existe de nombreux cas dans lesquels un compilateur moderne optimisera la vérification des limites imbriquées pour l’accès aux éléments tout en effectuant une itération sur un tableau à une dimension. Le résultat est un code qui avance simplement un pointeur d'index sur la mémoire contiguë du tableau. L'itération naïve sur des tableaux multidimensionnels implique généralement une couche supplémentaire de logique imbriquée, de sorte qu'un compilateur est moins susceptible d'optimiser l'opération. Ainsi, même si la surcharge liée à la vérification des limites d'accès à un seul élément est amortie par une durée d'exécution constante en ce qui concerne les dimensions et la taille des tableaux, l'exécution d'un test simple pour mesurer la différence peut prendre beaucoup plus de temps.
Je voudrais mettre à jour à ce sujet, car dans . Les tableaux multidimensionnels .NET Core sont plus rapides que les tableaux à dents de scie . J'ai exécuté les tests de John Leidegren et voici les résultats obtenus sur .NET Core 2.0 preview 2. J'ai augmenté la valeur de la dimension afin de rendre moins visibles les influences éventuelles des applications en arrière-plan.
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
J'ai examiné les désassemblages et voici ce que j'ai trouvé
jagged[i][j][k] = i * j * k;
avait besoin de 34 instructions pour s'exécuter
multi[i, j, k] = i * j * k;
avait besoin de 11 instructions pour s'exécuter
single[i * dim * dim + j * dim + k] = i * j * k;
avait besoin de 23 instructions pour s'exécuter
Je n'ai pas pu identifier pourquoi les tableaux unidimensionnels étaient encore plus rapides que les tableaux multidimensionnels, mais je suppose que cela est dû à une optimisation réalisée sur le processeur.
Les tableaux multidimensionnels sont des matrices de dimensions (n-1).
Donc, int[,] square = new int[2,2]
est une matrice carrée 2x2, int[,,] cube = new int [3,3,3]
est une matrice cube-carré 3x3. La proportionnalité n'est pas requise.
Les tableaux en dents de scie ne sont qu'un tableau de tableaux - un tableau où chaque cellule contient un tableau.
Donc, les MDA sont proportionnelles, JD n'est peut-être pas! Chaque cellule peut contenir un tableau de longueur arbitraire!
Cela a peut-être été mentionné dans les réponses ci-dessus, mais pas explicitement: avec un tableau irrégulier, vous pouvez utiliser array[row]
pour faire référence à une ligne entière de données, mais ceci n'est pas autorisé pour les tableaux multi-d.
En plus des autres réponses, notez qu'un tableau multidimensionnel est alloué en tant qu'objet volumineux sur le tas. Cela a des implications:
<gcAllowVeryLargeObjects>
pour les tableaux multidimensionnels avant que le problème ne se pose si vous utilisez uniquement des tableaux déchiquetés.J'analyse les fichiers .il générés par ildasm pour créer une base de données d'assemmblies, de classes, de méthodes et de procédures stockées à utiliser pour effectuer une conversion. Je suis tombé sur ce qui suit, qui a cassé mon analyse.
.method private hidebysig instance uint32[0...,0...]
GenerateWorkingKey(uint8[] key,
bool forEncryption) cil managed
Le livre Expert .NET 2.0 IL Assembler, de Serge Lidin, Apress, publié en 2006, chapitre 8, Types primitifs et signatures, p. 149-150, explique.
<type>[]
est appelé un vecteur de <type>
,
<type>[<bounds> [<bounds>**] ]
est appelé un tableau de <type>
**
signifie que peut être répété, [ ]
signifie optionnel.
Exemples: Soit <type> = int32
.
1) int32[...,...]
est un tableau à deux dimensions de limites inférieures non définies et de tailles
2) int32[2...5]
est un tableau à une dimension de borne inférieure 2 et de taille 4.
3) int32[0...,0...]
est un tableau à deux dimensions de bornes inférieures 0 et de taille indéfinie.
À M