C #에서 다차원 배열과 배열 배열의 차이점은 무엇입니까?


454

C #에서 다차원 배열 double[,]과 배열 배열의 차이점은 무엇입니까 double[][]?

차이가있는 경우 각각에 가장 적합한 사용법은 무엇입니까?


7
첫 번째 double[,]는 직사각형 배열이며 double[][]"지그재그 배열"이라고합니다. 첫 번째는 각 행에 대해 동일한 수의 "열"을 갖는 반면, 두 번째는 각 행에 대해 다른 수의 "열"을 갖습니다.
GreatAndPowerfulOz

답변:


334

배열 배열 (들쭉날쭉 한 배열)은 다차원 배열보다 빠르며보다 효과적으로 사용할 수 있습니다. 다차원 배열의 구문이 더 좋습니다.

들쭉날쭉 한 배열과 다차원 배열을 사용하여 간단한 코드를 작성한 다음 IL 디스어셈블러를 사용하여 컴파일 된 어셈블리를 검사하면 들쭉날쭉 한 (또는 1 차원) 배열의 저장 및 검색이 단순한 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
@ 존, 직접 측정하고 가정하지 마십시오.
Hosam Aly

2
@ 존 : 내 첫 반응도 나도 틀렸어-자세한 내용은 Hosams 질문을 참조하십시오.
Henk Holterman

38
다차원 배열은 논리적으로 더 효율적이어야하지만 JIT 컴파일러에 의한 구현은 그렇지 않습니다. 위의 코드는 루프에서 배열 액세스를 표시하지 않으므로 유용하지 않습니다.
ILoveFortran

3
@Henk Holterman-아래의 답변을 참조하십시오. Windows에서 들쭉날쭉 한 배열이 빠르지 만 이것이 CLR에만 해당하며 모노와 같은 경우가 아니라는 것을 알아야합니다 ...
John Leidegren

12
나는 이것이 오래된 질문이라는 것을 알고 있으며,이 질문을받은 이후 CLR이 다차원 배열에 최적화되어 있는지 궁금합니다.
Anthony Nichols

197

다차원 배열은 멋진 선형 메모리 레이아웃을 만드는 반면, 들쭉날쭉 한 배열은 몇 가지 추가 수준의 간접 성을 의미합니다.

jagged[3][6]들쭉날쭉 한 배열에서 값 을 찾는 방법 var jagged = new int[10][5]은 다음과 같습니다. 인덱스 3에서 요소를 찾고 (배열) 해당 배열에서 인덱스 6에서 요소를 찾습니다 (값). 이 경우 각 차원마다 추가 조회가 있습니다 (고가의 메모리 액세스 패턴).

다차원 배열은 메모리에 선형으로 배치되며 실제 값은 인덱스를 곱하여 구합니다. 그러나 array가 주어지면 다차원 배열 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 

첫 번째 행은 들쭉날쭉 한 배열의 타이밍이고, 두 번째 행은 다차원 배열과 세 번째 행을 보여줍니다. 프로그램은 아래와 같습니다. 참고로 모노로 테스트했습니다. (Windows 타이밍은 주로 CLR 구현 변형으로 인해 크게 다릅니다).

윈도우에서 들쭉날쭉 한 배열의 타이밍은 다차원 배열이 어떻게 생겼는지에 대한 내 자신의 해석과 거의 동일합니다 .'Single () '을 참조하십시오. 안타깝게도 Windows JIT 컴파일러는 정말 어리 석고 불행히도 이러한 성능 토론을 어렵게 만들고 불일치가 너무 많습니다.

이것들은 내가 창문에 얻은 ​​타이밍입니다. 여기서 같은 거래입니다. 첫 번째 행은 들쭉날쭉 한 배열, 두 번째 다차원 및 세 번째 다차원의 자체 구현입니다. 모노에 비해 창에서 이것이 얼마나 느린 지 주목하십시오.

  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에서 훨씬 더 최적화되었습니다. 경계 검사와 관련이있을 수 있지만, 이유와 상관없이 타이밍 및 벤치 마크에서는 들쭉날쭉 한 배열이 다차원 배열보다 액세스 속도가 빠르다는 것을 분명히 보여줍니다.
Hosam Aly

10
그러나 타이밍이 너무 작은 것 같습니다 (몇 밀리 초). 이 수준에서는 시스템 서비스 및 / 또는 드라이버로부터 많은 간섭을받습니다. 최소한 1 초 또는 2 초 정도 걸리면서 테스트를 더 크게 만드십시오.
Hosam Aly

8
@JohnLeidegren : 다차원 배열은 한 차원을 다른 차원보다 색인 할 때 더 잘 작동한다는 사실은 반세기 동안 이해되어 왔는데, 특정 차원 만 다른 요소는 연속적으로 메모리에 저장되고 많은 유형의 메모리 (과거에 저장 됨) 현재) 연속 항목에 액세스하는 것이 먼 항목에 액세스하는 것보다 빠릅니다. 나는 .net에서 당신이하고있는 마지막 첨자에 의해 최적의 결과 색인을 얻어야한다고 생각하지만, 첨자를 바꿔서 시간을 테스트하는 것은 어떤 경우에도 유익 할 수 있습니다.
supercat

16
@supercat : C #의 다차원 배열은 행 주요 순서 로 저장 되므로 메모리에 비 연속적으로 액세스하므로 첨자 순서를 바꾸는 속도가 느려집니다. BTW보고 된 시간이 더 이상 정확하지 않아, 들쭉날쭉 한 배열 (최신 .NET CLR에서 테스트)보다 다차원 배열의 경우 거의 두 배 빠른 속도를 얻습니다.
Amro

9
나는 이것이 약간 pedantic이라는 것을 알고 있지만 이것이 Windows vs Mono가 아니라 CLR vs Mono라고 언급해야합니다. 때로는 혼동하는 것 같습니다. 둘은 동일하지 않습니다. 모노는 Windows에서도 작동합니다.
Magus

70

간단히 다차원 배열은 DBMS의 테이블과 유사합니다.
배열 배열 (들쭉날쭉 한 배열)을 사용하면 각 요소가 동일한 유형의 가변 길이의 다른 배열을 보유하게 할 수 있습니다.

따라서 데이터 구조가 테이블 (고정 행 / 열)처럼 보이는 경우 다차원 배열을 사용할 수 있습니다. 들쭉날쭉 한 배열은 고정 요소이며 각 요소는 가변 길이의 배열을 보유 할 수 있습니다

예 : 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
이것은 무엇을 사용할지 결정할 때 실제로 중요한 것입니다.이 속도가 아니라 .. 정사각형 배열을 가질 때 속도가 중요한 요소가 될 수 있습니다.
Xaser

46

서문 : 이 의견은 okutane이 제공 한 답변 을 다루기위한 것이지만 SO의 바보 같은 명성 시스템으로 인해 해당 시스템에 게시 할 수 없습니다.

메소드 호출로 인해 하나가 다른 것보다 느리다는 주장이 올바르지 않습니다. 보다 복잡한 경계 검사 알고리즘으로 인해 하나가 다른 것보다 느립니다. IL이 아니라 컴파일 된 어셈블리를 보면이를 쉽게 확인할 수 있습니다. 예를 들어, 4.5 설치에서 eax 및 edx에 저장된 인덱스를 사용하여 ecx가 가리키는 2 차원 배열에 저장된 요소 (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]

여기에서 메소드 호출로 인한 오버 헤드가 없음을 알 수 있습니다. 0이 아닌 인덱스의 가능성으로 인해 경계 검사가 매우 복잡합니다. 0이 아닌 경우 sub, cmp 및 jmps를 제거하면 코드가 거의 해결됩니다 (x*y_max+y)*sizeof(ptr)+sizeof(array_header). 이 계산은 요소에 대한 임의 액세스를위한 다른 것만 큼 빠릅니다 (하나의 곱셈은 시프트로 대체 될 수 있습니다. 이것이 2 비트의 거듭 제곱으로 바이트 크기를 선택하는 전체 이유이기 때문입니다).

또 다른 문제는 최신 컴파일러가 단일 차원 배열을 반복하면서 요소 액세스에 대한 중첩 경계 검사를 최적화하는 경우가 많다는 것입니다. 결과는 기본적으로 배열의 연속 메모리보다 인덱스 포인터를 앞당기는 코드입니다. 다차원 배열에 대한 순진한 반복에는 일반적으로 중첩 된 논리의 추가 계층이 포함되므로 컴파일러가 작업을 최적화 할 가능성이 적습니다. 따라서 단일 요소에 액세스하는 범위 검사 오버 헤드가 배열 차원 및 크기와 관련하여 일정한 런타임으로 바뀌더라도 차이를 측정하는 간단한 테스트 사례를 실행하는 데 여러 시간이 걸릴 수 있습니다.


1
okutane (Dmitry 아님)의 답변을 수정 해 주셔서 감사합니다. 사람들이 Stackoverflow에 대한 잘못된 답변을하고 250 개의 투표를하는 반면 다른 사람들은 올바른 답변을하고 훨씬 적습니다. 그러나 결국 IL 코드는 관련이 없습니다. 성능에 대해 말하려면 속도를 실제로 측정해야합니다. 네가 했니? 나는 그 차이가 어리석은 것이라고 생각한다.
Elmue

38

.NET Core의 다차원 배열이 들쭉날쭉 한 배열보다 빠르기 때문에 이것을 업데이트하고 싶습니다 . John Leidegren 의 테스트를 실행했으며 .NET Core 2.0 Preview 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 개의 명령

왜 1 차원 배열이 여전히 다차원보다 더 빠른지 식별 할 수 없었지만 CPU에 대한 일부 최적화와 관련이 있다고 생각합니다.


14

다차원 배열은 (n-1) 차원 행렬입니다.

그래서 int[,] square = new int[2,2]정방 행렬 × 2이며, int[,,] cube = new int [3,3,3]정방 행렬 × 3 - 큐브입니다. 비례는 필요하지 않습니다.

들쭉날쭉 한 배열은 배열의 배열입니다. 각 셀에 배열이 포함 된 배열입니다.

따라서 MDA는 비례 적이며 JD는 그렇지 않을 수 있습니다! 각 셀은 임의 길이의 배열을 포함 할 수 있습니다!


7

이것은 위의 답변에서 언급되었지만 명시 적으로 언급되지 않았을 수 있습니다. 들쭉날쭉 한 배열을 사용 array[row]하면 전체 데이터 행을 참조하는 데 사용할 수 있지만 다중 d 배열에는 허용되지 않습니다.


4

다른 답변 외에도 다차원 배열은 힙에서 하나의 큰 청키 객체로 할당됩니다. 이것은 몇 가지 의미가 있습니다.

  1. 일부 다차원 배열은 그와 동등한 들쭉날쭉 한 배열 대응 물이없는 큰 객체 힙 (LOH)에 할당됩니다.
  2. GC는 다차원 배열을 할당하기 위해 하나의 연속 된 여유 메모리 블록을 찾아야하지만, 들쭉날쭉 한 배열은 힙 조각화로 인해 발생하는 간격을 채울 수 있습니다. 하지만 LOH는 기본적으로 압축되지 않습니다 (요청해야 할 때마다 요청해야 함).
  3. 당신은 보길 원하는 것 <gcAllowVeryLargeObjects>다차원 배열을위한 방법 은 오직 들쭉날쭉 배열을 사용하는 경우 문제가 지금까지 올 전에.

2

ildasm에 의해 생성 된 .il 파일을 구문 분석하여 변환을 수행하는 데 사용할 어셈블리, 클래스, 메서드 및 저장 프로 시저 데이터베이스를 작성합니다. 나는 파싱을 깨뜨린 다음을 발견했다.

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

Apress의 Serge Lidin, Apress의 Expert .NET 2.0 IL 어셈블러는 2006 년 8 장 프리미티브 유형 및 서명, 149-150 페이지를 설명합니다.

<type>[]의 벡터라고합니다 <type>.

<type>[<bounds> [<bounds>**] ] 의 배열이라고합니다 <type>

**수단은 반복 될 수 있고, [ ]선택적인 수단을 의미한다.

예 : Let <type> = int32.

1) int32[...,...]정의되지 않은 하한과 크기의 2 차원 배열

2) int32[2...5]는 하한 2와 크기 4의 1 차원 배열입니다.

3) int32[0...,0...]하한 0과 정의되지 않은 크기의 2 차원 배열입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.