Respostas:
A matriz de matrizes (matrizes irregulares) é mais rápida que as matrizes multidimensionais e pode ser usada com mais eficiência. Matrizes multidimensionais têm uma sintaxe melhor.
Se você escrever um código simples usando matrizes irregulares e multidimensionais e depois inspecionar o conjunto compilado com um desmontador de IL, verá que o armazenamento e a recuperação de matrizes irregulares (ou unidimensionais) são instruções simples de IL, enquanto as mesmas operações para matrizes multidimensionais são o método invocações sempre mais lentas.
Considere os seguintes métodos:
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;
}
A IL será a seguinte:
.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
Ao usar matrizes irregulares, você pode executar facilmente operações como troca e redimensionamento de linha. Talvez, em alguns casos, o uso de matrizes multidimensionais seja mais seguro, mas mesmo o Microsoft FxCop diz que matrizes irregulares devem ser usadas em vez de multidimensionais quando você a usa para analisar seus projetos.
Uma matriz multidimensional cria um bom layout de memória linear, enquanto uma matriz irregular implica vários níveis extras de indireção.
A procura do valor jagged[3][6]
em uma matriz irregular var jagged = new int[10][5]
funciona da seguinte maneira: Consulte o elemento no índice 3 (que é uma matriz) e procure o elemento no índice 6 nessa matriz (que é um valor). Para cada dimensão, neste caso, há uma pesquisa adicional (esse é um padrão de acesso à memória caro).
Uma matriz multidimensional é apresentada linearmente na memória, e o valor real é encontrado pela multiplicação dos índices. No entanto, dada a matriz var mult = new int[10,30]
, a Length
propriedade dessa matriz multidimensional retorna o número total de elementos, ou seja, 10 * 30 = 300.
A Rank
propriedade de uma matriz irregular é sempre 1, mas uma matriz multidimensional pode ter qualquer classificação. O GetLength
método de qualquer matriz pode ser usado para obter o comprimento de cada dimensão. Para a matriz multidimensional neste exemplo, mult.GetLength(1)
retorna 30.
A indexação da matriz multidimensional é mais rápida. por exemplo, considerando a matriz multidimensional neste exemplo mult[1,7]
= 30 * 1 + 7 = 37, obtenha o elemento nesse índice 37. Esse é um melhor padrão de acesso à memória porque apenas um local de memória está envolvido, que é o endereço base da matriz.
Uma matriz multidimensional, portanto, aloca um bloco de memória contínuo, enquanto uma matriz irregular não precisa ser quadrada, por exemplo jagged[1].Length
, não precisa ser igual jagged[2].Length
, o que seria verdadeiro para qualquer matriz multidimensional.
Em termos de desempenho, matrizes multidimensionais devem ser mais rápidas. Muito mais rápido, mas devido a uma implementação CLR muito ruim, eles não são.
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
A primeira linha são tempos de matrizes irregulares, a segunda mostra matrizes multidimensionais e a terceira, bem, é assim que deve ser. O programa é mostrado abaixo, FYI, este foi testado em execução mono. (O tempo das janelas é muito diferente, principalmente devido às variações de implementação do CLR).
Nas janelas, o tempo das matrizes irregulares é muito superior, quase o mesmo que a minha própria interpretação de como deve ser a matriz multidimensional, consulte 'Single ()'. Infelizmente, o compilador JIT do Windows é realmente estúpido, e isso infelizmente torna essas discussões de desempenho difíceis, existem muitas inconsistências.
Estes são os horários que obtive nas janelas, o mesmo negócio aqui, a primeira linha são matrizes irregulares, a segunda multidimensional e a terceira a minha própria implementação da multidimensional, observe o quanto isso é mais lento nas janelas em comparação com 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
Código fonte:
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();
}
}
Simplificando, matrizes multidimensionais são semelhantes a uma tabela no DBMS.
Matriz de matriz (matriz irregular) permite que cada elemento mantenha outra matriz do mesmo tipo de comprimento variável.
Portanto, se você tiver certeza de que a estrutura dos dados se parece com uma tabela (linhas / colunas fixas), poderá usar uma matriz multidimensional. Matriz irregular são elementos fixos e cada elemento pode conter uma matriz de comprimento variável
Por exemplo, Psuedocode:
int[,] data = new int[2,2];
data[0,0] = 1;
data[0,1] = 2;
data[1,0] = 3;
data[1,1] = 4;
Pense no exposto acima como uma tabela 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 };
Pense no exposto como cada linha com número variável de colunas:
1 | 2 | 3 | 4 11 | 12 21 | 22 | 23
Prefácio: Este comentário pretende abordar a resposta fornecida pelo okutane , mas devido ao sistema de reputação boba da SO, não posso publicá-lo onde ele pertence.
Sua afirmação de que uma é mais lenta que a outra por causa das chamadas de método não está correta. Um é mais lento que o outro por causa de algoritmos de verificação de limites mais complicados. Você pode verificar isso facilmente observando, não o IL, mas o assembly compilado. Por exemplo, na minha instalação 4.5, acessar um elemento (via ponteiro no edx) armazenado em uma matriz bidimensional apontada pelo ecx com índices armazenados no eax e edx é assim:
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]
Aqui, você pode ver que não há sobrecarga nas chamadas de método. A verificação de limites é muito complicada, graças à possibilidade de índices diferentes de zero, que é uma funcionalidade que não é oferecida com matrizes irregulares. Se removermos os sub, cmp e jmps para os casos diferentes de zero, o código será praticamente resolvido (x*y_max+y)*sizeof(ptr)+sizeof(array_header)
. Esse cálculo é tão rápido (uma multiplicação pode ser substituída por uma mudança, já que essa é a razão pela qual escolhemos bytes para serem dimensionados como potências de dois bits) como qualquer outra coisa para acesso aleatório a um elemento.
Outra complicação é que há muitos casos em que um compilador moderno otimiza a verificação de limites aninhados para acesso ao elemento enquanto itera sobre uma matriz de dimensão única. O resultado é um código que basicamente apenas avança um ponteiro de índice sobre a memória contígua da matriz. A iteração ingênua sobre matrizes multidimensionais geralmente envolve uma camada extra de lógica aninhada; portanto, é menos provável que um compilador otimize a operação. Portanto, mesmo que a sobrecarga de verificação de limites do acesso a um único elemento seja amortizada em tempo de execução constante em relação às dimensões e tamanhos da matriz, um caso de teste simples para medir a diferença pode demorar muito mais para ser executado.
Gostaria de atualizar isso, porque no .NET Core as matrizes multidimensionais são mais rápidas que as matrizes irregulares . Fiz os testes de John Leidegren e esses são os resultados da visualização 2. do .NET Core 2.0. Aumentei o valor da dimensão para tornar menos visíveis as possíveis influências de aplicativos em segundo plano.
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
Eu olhei para desmontagens e foi isso que eu achei
jagged[i][j][k] = i * j * k;
34 instruções necessárias para executar
multi[i, j, k] = i * j * k;
11 instruções necessárias para executar
single[i * dim * dim + j * dim + k] = i * j * k;
necessárias 23 instruções para executar
Não consegui identificar por que matrizes unidimensionais ainda eram mais rápidas que multidimensionais, mas meu palpite é que isso tem a ver com alguma otimização feita na CPU
Matrizes multidimensionais são matrizes de dimensão (n-1).
Assim int[,] square = new int[2,2]
é a matriz quadrada 2x2, int[,,] cube = new int [3,3,3]
é uma matriz cubo-quadrada 3x3. Proporcionalidade não é necessária.
Matrizes irregulares são apenas uma matriz de matrizes - uma matriz em que cada célula contém uma matriz.
Portanto, o MDA é proporcional, o JD pode não ser! Cada célula pode conter uma matriz de comprimento arbitrário!
Além das outras respostas, observe que uma matriz multidimensional é alocada como um grande objeto robusto no heap. Isso tem algumas implicações:
<gcAllowVeryLargeObjects>
matrizes multidimensionais muito antes que o problema ocorra, se você usar apenas matrizes irregulares.Estou analisando arquivos .il gerados pelo ildasm para criar um banco de dados de declarações, classes, métodos e procedimentos armazenados para uso na conversão. Me deparei com o seguinte, que interrompeu minha análise.
.method private hidebysig instance uint32[0...,0...]
GenerateWorkingKey(uint8[] key,
bool forEncryption) cil managed
O livro Expert .NET 2.0 IL Assembler, de Serge Lidin, Apress, publicado em 2006, capítulo 8, Tipos e assinaturas primitivas, pp. 149-150, explica.
<type>[]
é denominado vetor de <type>
,
<type>[<bounds> [<bounds>**] ]
é denominado uma matriz de <type>
**
meios podem ser repetidos, [ ]
meios opcionais.
Exemplos: Vamos <type> = int32
.
1) int32[...,...]
é uma matriz bidimensional de limites e tamanhos inferiores indefinidos
2) int32[2...5]
é uma matriz unidimensional do limite inferior 2 e tamanho 4.
3) int32[0...,0...]
é uma matriz bidimensional de limites inferiores 0 e tamanho indefinido.
Tom
double[,]
é uma matriz retangular, enquantodouble[][]
é conhecida como "matriz irregular". A primeira terá o mesmo número de "colunas" para cada linha, enquanto a segunda (potencialmente) terá um número diferente de "colunas" para cada linha.