2009-10-19 39 views
34

Tôi đã cố gắng để xác định chi phí của tiêu đề trên một mảng .NET (trong một quá trình 32-bit) sử dụng mã này:Phần trên của một mảng .NET?

long bytes1 = GC.GetTotalMemory(false); 
object[] array = new object[10000]; 
    for (int i = 0; i < 10000; i++) 
     array[i] = new int[1]; 
long bytes2 = GC.GetTotalMemory(false); 
array[0] = null; // ensure no garbage collection before this point 

Console.WriteLine(bytes2 - bytes1); 
// Calculate array overhead in bytes by subtracting the size of 
// the array elements (40000 for object[10000] and 4 for each 
// array), and dividing by the number of arrays (10001) 
Console.WriteLine("Array overhead: {0:0.000}", 
        ((double)(bytes2 - bytes1) - 40000)/10001 - 4); 
Console.Write("Press any key to continue..."); 
Console.ReadKey(); 

Kết quả là

204800 
    Array overhead: 12.478 

Trong một 32 quá trình bit, đối tượng [1] phải có cùng kích thước với int [1], nhưng trên thực tế, phí nhảy cao hơn 3,28 byte đến

237568 
    Array overhead: 15.755 

Bất cứ ai cũng biết tại sao?

(Bằng cách này, nếu có ai đó tò mò, chi phí cho các đối tượng không mảng, ví dụ (đối tượng) i trong vòng lặp ở trên, là khoảng 8 byte (8.384), tôi nghe 16 byte trong quy trình 64 bit.)

+2

Có phải trong bản dựng Gỡ lỗi hoặc Bản phát hành không? –

+0

Hmm, tôi thực sự không biết, tôi đã sử dụng SnippetCompiler. Khi tôi chuyển sang Visual Studio, kết quả thay đổi một chút: 11,92 cho int [1] và 15,94 cho đối tượng [1], bất kể đó là một bản dựng Debug hay Release. – Qwertie

+0

Ồ, tôi cũng đã thay đổi thành 100000 để giảm ảnh hưởng của độ chi tiết phân bổ trong trình quản lý bộ nhớ. – Qwertie

Trả lời

44

Dưới đây là một (IMO) chương trình ngắn nhưng đầy đủ hơi gọn gàng để chứng minh điều tương tự:

using System; 

class Test 
{ 
    const int Size = 100000; 

    static void Main() 
    { 
     object[] array = new object[Size]; 
     long initialMemory = GC.GetTotalMemory(true); 
     for (int i = 0; i < Size; i++) 
     { 
      array[i] = new string[0]; 
     } 
     long finalMemory = GC.GetTotalMemory(true); 
     GC.KeepAlive(array); 

     long total = finalMemory - initialMemory; 

     Console.WriteLine("Size of each element: {0:0.000} bytes", 
          ((double)total)/Size); 
    } 
} 

Nhưng tôi nhận được kết quả tương tự - overhead cho bất kỳ mảng kiểu tham chiếu là 16 byte, trong khi chi phí cho bất kỳ mảng giá trị nào là 12 byte. Tôi vẫn đang cố gắng giải thích tại sao đó là, với sự trợ giúp của đặc tả CLI. Đừng quên rằng các mảng kiểu tham chiếu là biến thể, có thể có liên quan ...

EDIT: Với sự giúp đỡ của cordbg, tôi có thể xác nhận câu trả lời của Brian - con trỏ kiểu của mảng kiểu tham chiếu là như nhau bất kể loại phần tử thực tế. Có lẽ có một số funkiness trong object.GetType() (đó là không ảo, hãy nhớ) để giải thích cho điều này.

Như vậy, với quy tắc:

object[] x = new object[1]; 
string[] y = new string[1]; 
int[] z = new int[1]; 
z[0] = 0x12345678; 
lock(z) {} 

Chúng tôi kết thúc với một cái gì đó như sau:

Variables: 
x=(0x1f228c8) <System.Object[]> 
y=(0x1f228dc) <System.String[]> 
z=(0x1f228f0) <System.Int32[]> 

Memory: 
0x1f228c4: 00000000 003284dc 00000001 00326d54 00000000 // Data for x 
0x1f228d8: 00000000 003284dc 00000001 00329134 00000000 // Data for y 
0x1f228ec: 00000000 00d443fc 00000001 12345678 // Data for z 

Lưu ý rằng tôi đã đổ bộ nhớ 1 từ trước giá trị của biến chính nó.

Đối xy, các giá trị là:

  • Khối đồng bộ, sử dụng cho các khóa mã băm (hoặc một khóa mỏng - xem bình luận của Brian)
  • Loại trỏ
  • Kích của mảng
  • Con trỏ kiểu phần tử
  • Tham chiếu rỗng (phần tử đầu tiên)

Đối z, các giá trị là:

  • Sync chặn
  • Loại trỏ
  • Kích thước của mảng
  • 0x12345678 (phần tử đầu tiên)

mảng kiểu giá trị khác nhau (byte [], int [] vv) kết thúc với các con trỏ kiểu khác nhau, trong khi tất cả các mảng kiểu tham chiếu sử dụng cùng kiểu po liên, nhưng có một con trỏ kiểu phần tử khác. Con trỏ kiểu phần tử có cùng giá trị như bạn sẽ tìm thấy như là con trỏ kiểu cho một đối tượng thuộc loại đó. Vì vậy, nếu chúng ta nhìn vào bộ nhớ của một đối tượng chuỗi trong chạy trên, nó sẽ có một con trỏ kiểu 0x00329134.

Từ trước con trỏ loại chắc chắn có một cái gì đó để làm với một trong hai màn hình hoặc mã băm: gọi GetHashCode() populates rằng bit của bộ nhớ, và tôi tin mặc định object.GetHashCode() có được một khối đồng bộ để đảm bảo băm đang độc đáo cho tuổi thọ của vật thể. Tuy nhiên, chỉ cần làm lock(x){} không làm bất cứ điều gì, điều đó làm tôi ngạc nhiên ...

Tất cả điều này chỉ hợp lệ đối với các loại "vectơ", trong CLR, loại "vectơ" là một mảng chiều có giới hạn thấp hơn 0. Các mảng khác sẽ có bố cục khác - cho một thứ, chúng sẽ cần giới hạn dưới được lưu trữ ...

Cho đến nay điều này đã được thử nghiệm, nhưng đây là phỏng đoán - lý do cho hệ thống đang được thực hiện theo cách của nó. Từ đây, tôi thực sự đoán.

  • Tất cả các mảng object[] đều có thể chia sẻ cùng một mã JIT. Chúng sẽ hoạt động theo cách tương tự về phân bổ bộ nhớ, truy cập mảng, thuộc tính Length và (quan trọng) bố trí các tham chiếu cho GC. So sánh điều đó với mảng loại giá trị, trong đó các loại giá trị khác nhau có thể có dấu chân "GC" khác nhau (ví dụ: có thể có byte và sau đó là tham chiếu, các giá trị khác sẽ không có tham chiếu nào cả, v.v.).
  • Mỗi lần bạn chỉ định giá trị trong một thời gian là object[] thời gian chạy cần kiểm tra xem nó có hợp lệ hay không. Nó cần phải kiểm tra xem loại đối tượng có tham chiếu bạn đang sử dụng cho giá trị phần tử mới có tương thích với loại phần tử của mảng hay không. Ví dụ:

    object[] x = new object[1]; 
    object[] y = new string[1]; 
    x[0] = new object(); // Valid 
    y[0] = new object(); // Invalid - will throw an exception 
    

Đây là hiệp phương sai tôi đã đề cập trước đó. Bây giờ cho rằng điều này sẽ xảy ra cho mỗi nhiệm vụ duy nhất, nó làm cho tinh thần để giảm số lượng các indirections. Đặc biệt, tôi nghi ngờ bạn không thực sự muốn thổi bộ nhớ cache bằng cách phải đi đến đối tượng kiểu cho mỗi assigment để có được kiểu phần tử. Tôi nghi ngờ (và x86, lắp ráp của tôi là không đủ tốt để xác minh điều này) rằng xét nghiệm này cái gì đó như:

  • Là giá trị được sao chép một tham chiếu null? Nếu vậy, đó là tốt. (Xong.)
  • Tìm nạp con trỏ loại của đối tượng mà điểm tham chiếu tại.
  • Con trỏ kiểu đó có giống với con trỏ kiểu phần tử (kiểm tra bình đẳng nhị phân đơn giản) không? Nếu vậy, đó là tốt. (Xong.)
  • Con trỏ kiểu đó có tương thích với con trỏ kiểu phần tử không? (Kiểm tra phức tạp hơn nhiều, với sự thừa kế và các giao diện liên quan.) Nếu vậy, điều đó là tốt - nếu không, hãy ném một ngoại lệ.

Nếu chúng ta có thể chấm dứt tìm kiếm trong ba bước đầu tiên, không có nhiều gián tiếp - điều này tốt cho điều gì đó sẽ xảy ra thường xuyên như các phép gán mảng. Không có điều nào trong số này cần phải xảy ra đối với các bài tập kiểu giá trị, bởi vì nó có thể kiểm chứng được.

Vì vậy, đó là lý do tại sao tôi tin rằng mảng loại tham chiếu hơi lớn hơn mảng loại giá trị.

Câu hỏi hay - thực sự thú vị khi nghiên cứu về nó :)

+0

Liên kết đầu tiên của Philip (http://www.codeproject.com/KB/dotnet/arrays.aspx) có câu trả lời: "Các kiểu tham chiếu cũng có trường loại phần tử đứng trước dữ liệu; nó dường như dư thừa kể từ khi mảng Sự hiện diện của kiểu phần tử như là một trường cho phép các thành phần loại thông tin được trích xuất nhanh chóng mà không có cuộc gọi và hàm gọi hàm ảo, điều quan trọng đối với các đối tượng như hiệp phương sai mảng. " – Qwertie

+0

Jon, tôi tin rằng tôi đã tìm thấy bốn byte bị thiếu. Xin vui lòng xem câu trả lời của tôi. Nếu bạn muốn tôi có thể thêm các bãi cần thiết từ WinDbg để minh họa. –

+0

Và btw +1 để làm nổi bật thực tế này. –

1

Vì quản lý heap (kể từ khi bạn xử lý GetTotalMemory) chỉ có thể phân bổ các khối khá lớn, sau đó được phân bổ bởi các đoạn nhỏ hơn cho các mục đích lập trình bởi CLR.

+0

Bộ nhớ không được phân bổ trong các trang 4 KB hoặc 8 KB? Trong trường hợp đó, lỗi tối đa là 2-4% trong ví dụ này. Tôi đã thử tăng kích thước thử nghiệm lên 100.000 mảng, nhưng điều này tạo ra sự khác biệt nhỏ: chi phí int [1] thay đổi thành 11.924 và chi phí đối tượng [1] là 15.938: vẫn còn chênh lệch 3 byte. – Qwertie

+0

@Qwertie: Trên thực tế CLR phân bổ các phần khá lớn từ hệ điều hành. Trên các phân đoạn 32 bit thường khoảng 16 MB. Các phân đoạn này sau đó được sử dụng làm bộ nhớ cho vùng được quản lý. –

+0

Rất tiếc, đó là sự khác biệt 4 byte! Một số dễ dàng hơn để giải thích ... – Qwertie

2

Tôi nghĩ rằng bạn đang đưa ra một số giả định bị lỗi trong khi đo, vì việc cấp phát bộ nhớ (qua GetTotalMemory) trong vòng lặp của bạn có thể khác với bộ nhớ cần thiết thực tế chỉ cho các mảng - bộ nhớ có thể được cấp phát trong các khối lớn hơn. được các đối tượng khác trong bộ nhớ được khai hoang trong vòng vv

Dưới đây là một số thông tin cho bạn trên mảng overhead:

+0

Các giả định bị lỗi như ...? – Qwertie

+1

Các giả định bị lỗi như - GetTotalMemory chỉ được tăng lên bởi các phân bổ mảng trong vòng lặp của bạn, và chỉ bằng cách exacly số tiền cần thiết. –

+0

Tôi không giả định điều đó. Tôi đã giả định rằng khi kích thước tiếp cận vô cùng, GetTotalMemory sẽ hội tụ với mức tiêu thụ bộ nhớ của các mảng, nhưng tôi nhận ra tôi đã không đi đủ gần đến vô cùng lúc đầu. – Qwertie

22

Mảng là loại tham chiếu. Tất cả các loại tham chiếu đều có hai từ bổ sung. Tham chiếu kiểu và trường chỉ mục SyncBlock, trong đó các thứ khác được sử dụng để triển khai các khóa trong CLR. Vì vậy, loại phí trên các loại tham chiếu là 8 byte trên 32 bit. Trên hết, bản thân mảng cũng lưu trữ độ dài là 4 byte khác. Điều này mang lại tổng chi phí lên đến 12 byte.

Và tôi vừa học được câu trả lời của Jon Skeet, các mảng tham chiếu có thêm 4 byte. Điều này có thể được xác nhận bằng cách sử dụng WinDbg. Nó chỉ ra rằng từ bổ sung là một tham chiếu kiểu khác cho kiểu được lưu trữ trong mảng. Tất cả các loại tham chiếu được lưu trữ nội bộ dưới dạng object[], với tham chiếu bổ sung cho đối tượng kiểu của loại thực tế. Vì vậy, string[] thực sự chỉ là một object[] với tham chiếu loại bổ sung cho loại string. Để biết chi tiết, vui lòng xem bên dưới.

Giá trị được lưu trữ trong mảng: Mảng tham chiếu chứa tham chiếu đến đối tượng, vì vậy mỗi mục trong mảng là kích thước của tham chiếu (nghĩa là 4 byte trên 32 bit). Mảng của các loại giá trị lưu trữ các giá trị nội tuyến và do đó mỗi phần tử sẽ chiếm kích thước của loại được đề cập.

Câu hỏi này cũng có thể quan tâm: C# List<double> size vs double[] size

Gory Chi tiết

Xét đoạn mã sau

var strings = new string[1]; 
var ints = new int[1]; 

strings[0] = "hello world"; 
ints[0] = 42; 

Gắn WinDbg cho thấy như sau:

Đầu tiên chúng ta hãy nhìn vào mảng giá trị.

0:000> !dumparray -details 017e2acc 
Name: System.Int32[] 
MethodTable: 63b9aa40 
EEClass: 6395b4d4 
Size: 16(0x10) bytes 
Array: Rank 1, Number of elements 1, Type Int32 
Element Methodtable: 63b9aaf0 
[0] 017e2ad4 
    Name: System.Int32 
    MethodTable 63b9aaf0 
    EEClass: 6395b548 
    Size: 12(0xc) bytes 
    (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) 
    Fields: 
      MT Field Offset     Type VT  Attr Value Name 
    63b9aaf0 40003f0  0   System.Int32 1 instance  42 m_value <=== Our value 

0:000> !objsize 017e2acc 
sizeof(017e2acc) =   16 (  0x10) bytes (System.Int32[]) 

0:000> dd 017e2acc -0x4 
017e2ac8 00000000 63b9aa40 00000001 0000002a <=== That's the value 

Đầu tiên chúng tôi đổ mảng và một phần tử có giá trị là 42. Có thể thấy kích thước là 16 byte. Đó là 4 byte cho giá trị int32, 8 byte cho chi phí tham chiếu thông thường và 4 byte khác cho độ dài của mảng.

Kết xuất thô hiển thị SyncBlock, bảng phương thức cho int[], độ dài và giá trị 42 (2a trong hex). Lưu ý rằng SyncBlock nằm ngay trước đối tượng tham chiếu.

Tiếp theo, hãy xem string[] để tìm hiểu xem từ bổ sung được sử dụng để làm gì.

0:000> !dumparray -details 017e2ab8 
Name: System.String[] 
MethodTable: 63b74ed0 
EEClass: 6395a8a0 
Size: 20(0x14) bytes 
Array: Rank 1, Number of elements 1, Type CLASS 
Element Methodtable: 63b988a4 
[0] 017e2a90 
    Name: System.String 
    MethodTable: 63b988a4 
    EEClass: 6395a498 
    Size: 40(0x28) bytes <=== Size of the string 
    (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) 
    String:  hello world  
    Fields: 
      MT Field Offset     Type VT  Attr Value Name 
    63b9aaf0 4000096  4   System.Int32 1 instance  12 m_arrayLength 
    63b9aaf0 4000097  8   System.Int32 1 instance  11 m_stringLength 
    63b99584 4000098  c   System.Char 1 instance  68 m_firstChar 
    63b988a4 4000099  10  System.String 0 shared static Empty 
    >> Domain:Value 00226438:017e1198 << 
    63b994d4 400009a  14  System.Char[] 0 shared static WhitespaceChars 
    >> Domain:Value 00226438:017e1760 << 

0:000> !objsize 017e2ab8 
sizeof(017e2ab8) =   60 (  0x3c) bytes (System.Object[]) <=== Notice the underlying type of the string[] 

0:000> dd 017e2ab8 -0x4 
017e2ab4 00000000 63b74ed0 00000001 63b988a4 <=== Method table for string 
017e2ac4 017e2a90 <=== Address of the string in memory 

0:000> !dumpmt 63b988a4 
EEClass: 6395a498 
Module: 63931000 
Name: System.String 
mdToken: 02000024 (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) 
BaseSize: 0x10 
ComponentSize: 0x2 
Number of IFaces in IFaceMap: 7 
Slots in VTable: 196 

Trước tiên, chúng tôi đổ mảng và chuỗi. Tiếp theo chúng ta đổ kích thước của string[]. Lưu ý rằng WinDbg liệt kê loại là System.Object[] tại đây. Kích thước đối tượng trong trường hợp này bao gồm chuỗi chính nó, vì vậy tổng kích thước là 20 từ mảng cộng với 40 cho chuỗi.

Bằng cách bán các byte thô của cá thể, chúng ta có thể thấy như sau: Đầu tiên chúng ta có SyncBlock, sau đó theo bảng phương thức object[], sau đó là độ dài của mảng. Sau đó chúng ta tìm thêm 4 byte với tham chiếu đến bảng phương thức cho chuỗi. Điều này có thể được xác nhận bởi lệnh dumpmt như được hiển thị ở trên. Cuối cùng, chúng ta tìm thấy tham chiếu duy nhất cho cá thể chuỗi thực tế.

Tóm lại

Các nguyên cần thiết cho mảng có thể được chia như sau (trên 32 bit đó là)

  • 4 byte SyncBlock
  • 4 byte cho Phương pháp bảng (loại tài liệu tham khảo) cho chính mảng
  • 4 byte cho Độ dài mảng
  • Mảng các loại tham chiếu thêm 4 byte khác để giữ m bảng ethod của loại phần tử thực tế (mảng loại tham chiếu là object[] dưới mui xe)

I.e. chi phí là 12 byte cho mảng loại giá trị16 byte cho mảng loại tham chiếu.

+0

đối tượng [1] và int [1] là hai loại tham chiếu. – Qwertie

+0

Tôi nghĩ bạn có nghĩa là "_two_ trường bổ sung" –

+0

@Henk: Typo. Cảm ơn! –

0

Tôi xin lỗi vì người khiếm thị nhưng tôi đã tìm thấy thông tin thú vị về bộ nhớ tràn ngập chỉ vào sáng hôm nay.

Chúng tôi có một dự án có số lượng lớn dữ liệu (tối đa 2GB).Là bộ nhớ chính chúng tôi sử dụng Dictionary<T,T>. Hàng ngàn từ điển được tạo ra thực sự. Sau khi thay đổi nó thành List<T> cho các phím và List<T> cho các giá trị (chúng tôi đã thực hiện IDictionary<T,T> chính mình) việc sử dụng bộ nhớ giảm khoảng 30-40%.

Tại sao?

+1

Nếu bạn có một cái nhìn vào lớp từ điển trong Reflector, nó sử dụng một mảng nội bộ của Entry structs. Mỗi cấu trúc, cũng như các đối tượng khóa/giá trị, có 'hashcode' và 'int' tiếp theo, được sử dụng bởi việc triển khai từ điển. Nếu lớp IDictionary của bạn không có dữ liệu bổ sung cho mỗi mục nhập, hai int đó có thể là nơi sử dụng bộ nhớ bổ sung của bạn xuất phát từ – thecoop

+1

Từ điển trong nội bộ sử dụng cấu trúc được gọi là Entry, sử dụng bộ nhớ bổ sung để giữ trường "hashCode" và "next" cho mỗi mục. Tôi con số này có thể được sử dụng để giải quyết va chạm băm. – Qwertie

+1

Ngoài ra còn có một loạt các "số lượng" số nguyên có cùng chiều dài với mảng Các mục nhập, cộng thêm 4 byte trên đầu. Do đó, tôi mong đợi một Danh sách KeyValuePair sẽ nhỏ hơn 12 byte cho mỗi mục nhập (thường) so với từ điển , vì ba giá trị hashCode, tiếp theo và giá trị "xô". – Qwertie

Các vấn đề liên quan