Câu trả lời:
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.
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 Length
tính của mảng đa chiều đó trả về tổng số phần tử tức là 10 * 30 = 300.
Các Rank
tà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 GetLength
phươ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].Length
khô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 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();
}
}
Đơ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
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.
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
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 ý!
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 ý:
<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.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
double[,]
là một mảng hình chữ nhật, trong khidouble[][]
đượ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.