Sự khác biệt giữa một mảng nhiều chiều và một mảng các mảng trong C # là gì?


454

Sự khác biệt giữa mảng nhiều chiều double[,]và mảng mảng double[][]trong C # là gì?

Nếu có sự khác biệt, cách sử dụng tốt nhất cho mỗi người là gì?


7
Đầu tiên double[,]là một mảng hình chữ nhật, trong khi double[][]được gọi là "mảng răng cưa". Cái đầu tiên sẽ có cùng số "cột" cho mỗi hàng, trong khi cái thứ hai (có khả năng) có số lượng "cột" khác nhau cho mỗi hàng.
GreatAndPowerfulOz

Câu trả lời:


334

Mảng mảng (mảng lởm chởm) nhanh hơn mảng đa chiều và có thể được sử dụng hiệu quả hơn. Mảng nhiều chiều có cú pháp đẹp hơn.

Nếu bạn viết một số mã đơn giản bằng cách sử dụng các mảng lởm chởm và đa chiều và sau đó kiểm tra tập hợp đã biên dịch bằng trình dịch ngược IL, bạn sẽ thấy rằng việc lưu trữ và truy xuất từ ​​các mảng lởm chởm (hoặc một chiều) là các lệnh IL đơn giản trong khi các thao tác tương tự cho các mảng đa chiều là phương thức các yêu cầu luôn luôn chậm hơn.

Hãy xem xét các phương pháp sau:

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 của họ sẽ như sau:

.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

Khi sử dụng mảng răng cưa, bạn có thể dễ dàng thực hiện các thao tác như hoán đổi hàng và thay đổi kích thước hàng. Có thể trong một số trường hợp, việc sử dụng mảng đa chiều sẽ an toàn hơn, nhưng ngay cả Microsoft FxCop cũng nói rằng các mảng lởm chởm nên được sử dụng thay vì đa chiều khi bạn sử dụng nó để phân tích các dự án của mình.


7
@ John, hãy tự đo chúng và đừng đưa ra giả định.
Hosam Aly

2
@ John: Phản ứng đầu tiên của tôi cũng vậy nhưng tôi đã nhầm - xem chi tiết câu hỏi của Hosams.
Henk Holterman

38
Các mảng đa chiều sẽ hợp lý hơn nhưng hiệu quả của chúng bởi trình biên dịch JIT thì không. Đoạn mã trên không hữu ích vì nó không hiển thị truy cập mảng trong một vòng lặp.
ILoveFortran

3
@Henk Holterman - Xem câu trả lời của tôi dưới đây, Nó có thể là trường hợp đó trên cửa sổ lởm chởm mảng là nhanh nhưng người ta phải nhận ra rằng đây là cụ thể hoàn toàn CLR và không phải là trường hợp với ví dụ mono ...
John Leidegren

12
Tôi biết đây là một câu hỏi cũ, chỉ tự hỏi liệu CLR đã được tối ưu hóa cho các mảng đa chiều kể từ khi câu hỏi này được hỏi.
Anthony Nichols

197

Một mảng nhiều chiều tạo ra một bố cục bộ nhớ tuyến tính đẹp trong khi một mảng lởm chởm hàm ý thêm một số mức độ gián tiếp.

Tra cứu giá trị jagged[3][6]trong một mảng lởm chởm var jagged = new int[10][5]như thế này: Tra cứu phần tử tại chỉ mục 3 (là một mảng) và tra cứu phần tử tại chỉ mục 6 trong mảng đó (là một giá trị). Đối với mỗi thứ nguyên trong trường hợp này, sẽ có một tra cứu bổ sung (đây là mẫu truy cập bộ nhớ đắt tiền).

Một mảng nhiều chiều được đặt tuyến tính trong bộ nhớ, giá trị thực được tìm thấy bằng cách nhân các chỉ số với nhau. Tuy nhiên, với mảng var mult = new int[10,30], thuộc Lengthtính của mảng đa chiều đó trả về tổng số phần tử tức là 10 * 30 = 300.

Các Ranktài sản của một mảng lởm chởm luôn là 1, nhưng một mảng đa chiều có thể có bất cứ cấp bậc. Các GetLengthphương pháp của bất kỳ mảng có thể được sử dụng để có được chiều dài của mỗi chiều. Đối với mảng đa chiều trong ví dụ này mult.GetLength(1)trả về 30.

Lập chỉ mục mảng đa chiều nhanh hơn. ví dụ: đưa ra mảng đa chiều trong ví dụ này mult[1,7]= 30 * 1 + 7 = 37, lấy phần tử ở chỉ số đó 37. Đây là mẫu truy cập bộ nhớ tốt hơn vì chỉ có một vị trí bộ nhớ được tham gia, là địa chỉ cơ sở của mảng.

Do đó, một mảng nhiều chiều phân bổ một khối bộ nhớ liên tục, trong khi một mảng lởm chởm không phải là hình vuông, ví dụ jagged[1].Lengthkhông phải bằng nhau jagged[2].Length, điều này sẽ đúng với bất kỳ mảng đa chiều nào.

Hiệu suất

Hiệu suất khôn ngoan, mảng đa chiều nên nhanh hơn. Nhanh hơn rất nhiều, nhưng do triển khai CLR thực sự tồi nên họ không làm được.

 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 

Hàng đầu tiên là định thời của các mảng lởm chởm, hàng thứ hai hiển thị các mảng đa chiều và hàng thứ ba, đó là cách nó phải như vậy. Chương trình được hiển thị dưới đây, FYI này đã được thử nghiệm chạy đơn. (Thời gian của các cửa sổ rất khác nhau, chủ yếu là do các biến thể triển khai CLR).

Trên các cửa sổ, thời gian của các mảng lởm chởm vượt trội hơn rất nhiều, giống như cách giải thích của riêng tôi về mảng đa chiều tìm kiếm sẽ như thế nào, xem 'Đơn ()'. Đáng buồn thay, trình biên dịch JIT của windows thực sự ngu ngốc, và điều này không may làm cho các cuộc thảo luận về hiệu suất này trở nên khó khăn, có quá nhiều mâu thuẫn.

Đây là các thời gian tôi nhận được trên các cửa sổ, cùng một giao dịch ở đây, hàng đầu tiên là các mảng lởm chởm, đa chiều thứ hai và thứ ba do tôi thực hiện đa chiều, lưu ý rằng tốc độ này trên các cửa sổ chậm hơn bao nhiêu so với đơn âm.

  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

Mã nguồn:

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
Hãy thử định thời gian cho chính họ, và xem cả hai thực hiện như thế nào. Mảng răng cưa được tối ưu hóa hơn nhiều trong .NET. Nó có thể liên quan đến kiểm tra giới hạn, nhưng bất kể lý do, thời gian và điểm chuẩn cho thấy rõ ràng rằng các mảng lởm chởm có thể truy cập nhanh hơn so với các chiều đa chiều.
Hosam Aly

10
Nhưng thời gian của bạn dường như quá nhỏ (vài mili giây). Ở cấp độ này, bạn sẽ có nhiều can thiệp từ các dịch vụ hệ thống và / hoặc trình điều khiển. Làm cho bài kiểm tra của bạn lớn hơn nhiều, ít nhất là mất một hoặc hai giây.
Hosam Aly

8
@JohnLeidegren: Thực tế là mảng đa chiều hoạt động tốt hơn khi lập chỉ mục một chiều so với chiều khác đã được hiểu trong nửa thế kỷ, vì các yếu tố chỉ khác nhau ở một chiều cụ thể sẽ được lưu trữ liên tiếp trong bộ nhớ và với nhiều loại bộ nhớ (quá khứ và hiện tại), truy cập các mục liên tiếp nhanh hơn truy cập các mục ở xa. Tôi nghĩ trong .net, người ta sẽ nhận được chỉ mục kết quả tối ưu theo chỉ mục cuối cùng, đó là những gì bạn đang làm, nhưng việc kiểm tra thời gian với các đăng ký được hoán đổi có thể là thông tin trong mọi trường hợp.
supercat

16
@supercat: mảng đa chiều trong C # được lưu theo thứ tự chính hàng , việc hoán đổi thứ tự của các mục con sẽ chậm hơn vì bạn sẽ truy cập vào bộ nhớ không liên tục. BTW thời gian được báo cáo không còn chính xác nữa, tôi nhận được thời gian nhanh gấp gần hai lần so với mảng đa chiều so với mảng bị lởm chởm (được thử nghiệm trên .NET CLR mới nhất), đó là cách nó phải diễn ra ..
Amro

9
Tôi biết điều này hơi khoa trương, nhưng tôi phải đề cập rằng đây không phải là Windows vs Mono, mà là CLR vs Mono. Đôi khi bạn dường như nhầm lẫn những điều đó. Hai cái không tương đương nhau; Mono cũng hoạt động trên Windows.
Magus

70

Đơn giản chỉ cần đặt các mảng nhiều chiều tương tự như một bảng trong DBMS.
Mảng của mảng (mảng răng cưa) cho phép bạn có mỗi phần tử giữ một mảng khác có cùng độ dài biến.

Vì vậy, nếu bạn chắc chắn rằng cấu trúc dữ liệu trông giống như một bảng (các hàng / cột cố định), bạn có thể sử dụng một mảng nhiều chiều. Mảng răng cưa là các phần tử cố định và mỗi phần tử có thể chứa một mảng có độ dài thay đổi

Ví dụ: Psuedocode:

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

Hãy nghĩ về những điều trên như một bảng 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 }; 

Hãy nghĩ về điều trên vì mỗi hàng có số lượng cột khác nhau:

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

4
đây là điều thực sự quan trọng khi quyết định sử dụng cái gì .. không phải tốc độ này .. tốc độ cũng có thể trở thành một yếu tố khi bạn có một mảng vuông.
Xaser

46

Lời nói đầu: Nhận xét này nhằm giải quyết câu trả lời được cung cấp bởi okutane , nhưng vì hệ thống danh tiếng ngớ ngẩn của SO, tôi không thể đăng nó ở nơi nó thuộc về.

Bạn khẳng định rằng cái này chậm hơn cái kia vì các cuộc gọi phương thức không đúng. Một cái chậm hơn cái kia vì các thuật toán kiểm tra giới hạn phức tạp hơn. Bạn có thể dễ dàng xác minh điều này bằng cách xem, không phải tại IL, mà là ở phần tổng hợp. Ví dụ: trong cài đặt 4.5 của tôi, việc truy cập một phần tử (thông qua con trỏ trong edx) được lưu trữ trong một mảng hai chiều được chỉ ra bởi ecx với các chỉ mục được lưu trữ trong eax và edx trông giống như vậy:

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]

Ở đây, bạn có thể thấy rằng không có chi phí nào từ các cuộc gọi phương thức. Việc kiểm tra giới hạn rất phức tạp nhờ khả năng của các chỉ số khác không, đây là một chức năng không được cung cấp với các mảng lởm chởm. Nếu chúng ta loại bỏ các phụ, cmp và jmps cho các trường hợp khác không, mã sẽ giải quyết được khá nhiều (x*y_max+y)*sizeof(ptr)+sizeof(array_header). Tính toán này nhanh như vậy (một bội số có thể được thay thế bằng một ca, vì đó là toàn bộ lý do chúng tôi chọn byte có kích thước là lũy thừa của hai bit) như mọi thứ khác để truy cập ngẫu nhiên vào một phần tử.

Một sự phức tạp khác là có rất nhiều trường hợp trong đó một trình biên dịch hiện đại sẽ tối ưu hóa các giới hạn lồng nhau - kiểm tra truy cập phần tử trong khi lặp qua một mảng một chiều. Kết quả là mã về cơ bản chỉ cần tiến một con trỏ chỉ mục qua bộ nhớ liền kề của mảng. Lặp lại ngây thơ trên các mảng đa chiều thường liên quan đến một lớp logic lồng nhau, do đó trình biên dịch ít có khả năng tối ưu hóa hoạt động. Vì vậy, mặc dù chi phí kiểm tra giới hạn của việc truy cập một phần tử duy nhất được phân bổ vào thời gian chạy liên tục theo kích thước và kích thước mảng, một trường hợp thử nghiệm đơn giản để đo sự khác biệt có thể mất nhiều thời gian hơn để thực thi.


1
Cảm ơn đã sửa câu trả lời của okutane (không phải Dmitry). Thật khó chịu khi mọi người đưa ra câu trả lời sai trên Stackoverflow và nhận được 250 phiếu bầu trong khi những người khác đưa ra câu trả lời đúng và nhận được ít hơn rất nhiều. Nhưng cuối cùng, mã IL không liên quan. Bạn phải thực sự ĐO LƯỜNG tốc độ để nói bất cứ điều gì về hiệu suất. Bạn đã làm điểu đó? Tôi nghĩ rằng sự khác biệt sẽ là vô lý.
Elmue

38

Tôi muốn cập nhật về điều này, bởi vì trong các mảng đa chiều của .NET Core nhanh hơn các mảng lởm chởm . Tôi đã chạy thử nghiệm từ John Leidegren và đây là những kết quả trên bản xem trước .NET Core 2.0 2. Tôi đã tăng giá trị thứ nguyên để làm cho mọi ảnh hưởng có thể có từ các ứng dụng nền ít nhìn thấy hơn.

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 

Tôi đã xem xét các vấn đề và đây là những gì tôi tìm thấy

jagged[i][j][k] = i * j * k; cần 34 hướng dẫn để thực hiện

multi[i, j, k] = i * j * k; cần 11 hướng dẫn để thực hiện

single[i * dim * dim + j * dim + k] = i * j * k; cần 23 hướng dẫn để thực hiện

Tôi không thể xác định lý do tại sao mảng một chiều vẫn nhanh hơn đa chiều nhưng tôi đoán là nó phải làm với một số tối ưu hóa được thực hiện trên CPU


14

Mảng đa chiều là ma trận độ cao (n-1).

Vậy int[,] square = new int[2,2]là ma trận vuông 2x2, int[,,] cube = new int [3,3,3]là một khối vuông - ma trận vuông 3x3. Tỷ lệ là không cần thiết.

Mảng răng cưa chỉ là mảng mảng - một mảng trong đó mỗi ô chứa một mảng.

Vì vậy, MDA là tỷ lệ thuận, JD có thể không! Mỗi ô có thể chứa một mảng có độ dài tùy ý!


7

Điều này có thể đã được đề cập trong các câu trả lời ở trên nhưng không rõ ràng: với mảng răng cưa bạn có thể sử dụng array[row]để tham chiếu toàn bộ một hàng dữ liệu, nhưng điều này không được phép đối với mảng nhiều d.


4

Ngoài các câu trả lời khác, lưu ý rằng một mảng nhiều chiều được phân bổ là một đối tượng chunky lớn trên heap. Điều này có một số hàm ý:

  1. Một số mảng đa chiều sẽ được phân bổ trên Heap đối tượng lớn (LOH) trong đó các đối tác mảng răng cưa tương đương của chúng sẽ không có.
  2. GC sẽ cần tìm một khối bộ nhớ trống liền kề duy nhất để phân bổ một mảng nhiều chiều, trong khi đó một mảng lởm chởm có thể có thể lấp đầy các khoảng trống do phân mảnh heap ... đây thường không phải là vấn đề trong .NET vì sự nén , nhưng LOH không được nén theo mặc định (bạn phải yêu cầu và bạn phải hỏi mỗi khi bạn muốn).
  3. Bạn sẽ muốn xem xét <gcAllowVeryLargeObjects>đối với mảng đa chiều chiều trước khi vấn đề này bao giờ sẽ đi lên nếu bạn chỉ sử dụng từng mảng lởm chởm.

2

Tôi đang phân tích cú pháp các tệp .il được tạo bởi ildasm để xây dựng cơ sở dữ liệu gồm các cụm, lớp, phương thức và các thủ tục được lưu trữ để sử dụng thực hiện chuyển đổi. Tôi đã đi qua sau đây, mà đã phá vỡ phân tích cú pháp của tôi.

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

Cuốn sách Expert .NET 2.0 IL Trình biên dịch, của Serge Lidin, Apress, xuất bản 2006, Chương 8, Các kiểu nguyên thủy và Chữ ký, trang 149-150 giải thích.

<type>[]được gọi là Vector của <type>,

<type>[<bounds> [<bounds>**] ] được gọi là một mảng của <type>

**có nghĩa là có thể được lặp đi lặp lại, [ ]có nghĩa là tùy chọn.

Ví dụ: Hãy <type> = int32.

1) int32[...,...]là một mảng hai chiều của giới hạn và kích thước thấp hơn không xác định

2) int32[2...5]là một mảng một chiều của giới hạn dưới 2 và kích thước 4.

3) int32[0...,0...]là một mảng hai chiều của giới hạn dưới 0 và kích thước không xác định.

Tom

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.