Quais são as diferenças entre uma matriz multidimensional e uma matriz de matrizes em C #?


454

Quais são as diferenças entre matrizes multidimensionais double[,]e array de matrizes double[][]em C #?

Se houver alguma diferença, qual é o melhor uso para cada um?


7
A primeira double[,]é uma matriz retangular, enquanto double[][]é 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.
GreatAndPowerfulOz 19/08/19

Respostas:


334

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.


7
@ John, meça você mesmo e não faça suposições.
28909 Hosam Aly

2
@ John: Minha primeira reação também, mas eu estava errado - veja a pergunta de Hosams para obter detalhes.
Henk Holterman

38
Matrizes multidimensionais devem logicamente ser mais eficazes, mas sua implementação pelo compilador JIT não é. O código acima não é útil, pois não mostra o acesso à matriz em um loop.
ILoveFortran # 28/02/09

3
@Henk Holterman - Veja minha resposta abaixo, pode ser o caso de que em janelas irregulares matrizes são rápidos, mas é preciso perceber que este é específico inteiramente CLR e não o caso com por exemplo mono ...
John Leidegren

12
Eu sei que essa é uma pergunta antiga, apenas imaginando se o CLR foi otimizado para matrizes multidimensionais desde que essa pergunta foi feita.
Anthony Nichols

197

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 Lengthpropriedade dessa matriz multidimensional retorna o número total de elementos, ou seja, 10 * 30 = 300.

A Rankpropriedade de uma matriz irregular é sempre 1, mas uma matriz multidimensional pode ter qualquer classificação. O GetLengthmé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.

atuação

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();
    }
}

2
Tente cronometrar você mesmo e veja como os dois se saem. Matrizes irregulares são muito mais otimizadas no .NET. Pode estar relacionado à verificação de limites, mas, independentemente do motivo, os tempos e as referências mostram claramente que as matrizes irregulares são mais rápidas de acessar do que as multidimensionais.
396 Hosam Aly

10
Mas seus tempos parecem muito pequenos (alguns milissegundos). Nesse nível, você terá muita interferência dos serviços e / ou drivers do sistema. Faça seus testes muito maiores, levando pelo menos um ou dois segundos.
279 Hosam Aly

8
@JohnLeidegren: O fato de matrizes multidimensionais funcionarem melhor ao indexar uma dimensão do que outra é entendido há meio século, uma vez que elementos que diferem em apenas uma dimensão específica serão armazenados consecutivamente na memória e com muitos tipos de memória (passado e presente), acessar itens consecutivos é mais rápido do que acessar itens distantes. Acho que no .net deve-se obter ótimos índices de indexação pelo último subscrito, o que você estava fazendo, mas testar o tempo com os subscritos trocados pode ser informativo em qualquer caso.
Supercat

16
@ supercat: matrizes multidimensionais em C # são armazenadas em ordem de maior linha , a troca da ordem dos subscritos seria mais lenta, pois você acessaria a memória de forma não consecutiva. Entre os tempos relatados não são mais precisos, recebo quase o dobro de vezes em matrizes multidimensionais do que em matrizes irregulares (testadas no .NET CLR mais recente), que é como deve ser ..
Amro

9
Eu sei que isso é um pouco pedante, mas devo mencionar que não é o Windows vs Mono, mas o CLR vs Mono. Você às vezes parece confundir isso. Os dois não são equivalentes; O Mono também funciona no Windows.
Magus

70

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

4
isto é o que realmente importa ao decidir o que usar .. não essa velocidade, bem ... a velocidade pode se tornar um fator quando você tem uma matriz quadrada.
Xaser

46

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.


1
Obrigado por corrigir a resposta de okutane (não Dmitry). É irritante que as pessoas dêem respostas erradas no Stackoverflow e recebam 250 votos positivos, enquanto outras dão respostas corretas e recebam muito menos. Mas, no final, o código IL é irrelevante. Você precisa realmente medir a velocidade para dizer algo sobre desempenho. Você fez isso? Eu acho que a diferença será ridícula.
Elmue

38

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


14

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!


7

Isso pode ter sido mencionado nas respostas acima, mas não explicitamente: com uma matriz irregular, você pode usar array[row]para referenciar uma linha inteira de dados, mas isso não é permitido para matrizes multi-d.


4

Além das outras respostas, observe que uma matriz multidimensional é alocada como um grande objeto robusto no heap. Isso tem algumas implicações:

  1. Algumas matrizes multidimensionais serão alocadas no Large Object Heap (LOH), onde suas contrapartes irregulares equivalentes da matriz não teriam.
  2. O GC precisará encontrar um único bloco de memória livre contíguo para alocar uma matriz multidimensional, enquanto uma matriz irregular pode ser capaz de preencher as lacunas causadas pela fragmentação de heap ... isso geralmente não é um problema no .NET devido à compactação. , mas o LOH não é compactado por padrão (você precisa solicitá-lo e solicitá-lo sempre que quiser).
  3. Você deve procurar <gcAllowVeryLargeObjects>matrizes multidimensionais muito antes que o problema ocorra, se você usar apenas matrizes irregulares.

2

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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.