Tại sao căn chỉnh cấu trúc phụ thuộc vào việc một loại trường là nguyên thủy hay do người dùng xác định?


121

Trong Noda Time v2, chúng tôi đang chuyển sang độ phân giải nano giây. Điều đó có nghĩa là chúng tôi không còn có thể sử dụng số nguyên 8 byte để đại diện cho toàn bộ khoảng thời gian mà chúng tôi quan tâm. Điều đó đã thúc đẩy tôi điều tra việc sử dụng bộ nhớ của (nhiều) cấu trúc của Noda Time, điều này đã dẫn tôi để phát hiện ra một chút kỳ lạ trong quyết định liên kết của CLR.

Thứ nhất, tôi nhận thấy rằng đây một quyết định triển khai và hành vi mặc định có thể thay đổi bất cứ lúc nào. Tôi nhận ra rằng tôi có thể sửa đổi nó bằng cách sử dụng [StructLayout][FieldOffset], nhưng tôi muốn đưa ra một giải pháp không yêu cầu điều đó nếu có thể.

Kịch bản cốt lõi của tôi là tôi có một trường structchứa một trường kiểu tham chiếu và hai trường kiểu giá trị khác, trong đó những trường đó là trình bao bọc đơn giản int. Tôi đã hy vọng rằng nó sẽ được biểu diễn dưới dạng 16 byte trên CLR 64 bit (8 cho tham chiếu và 4 cho mỗi cái khác), nhưng vì một số lý do nó sử dụng 24 byte. Nhân tiện, tôi đang đo không gian bằng cách sử dụng các mảng - tôi hiểu rằng bố cục có thể khác nhau trong các tình huống khác nhau, nhưng điều này cảm thấy như một điểm khởi đầu hợp lý.

Đây là một chương trình mẫu chứng minh vấn đề:

using System;
using System.Runtime.InteropServices;

#pragma warning disable 0169

struct Int32Wrapper
{
    int x;
}

struct TwoInt32s
{
    int x, y;
}

struct TwoInt32Wrappers
{
    Int32Wrapper x, y;
}

struct RefAndTwoInt32s
{
    string text;
    int x, y;
}

struct RefAndTwoInt32Wrappers
{
    string text;
    Int32Wrapper x, y;
}    

class Test
{
    static void Main()
    {
        Console.WriteLine("Environment: CLR {0} on {1} ({2})",
            Environment.Version,
            Environment.OSVersion,
            Environment.Is64BitProcess ? "64 bit" : "32 bit");
        ShowSize<Int32Wrapper>();
        ShowSize<TwoInt32s>();
        ShowSize<TwoInt32Wrappers>();
        ShowSize<RefAndTwoInt32s>();
        ShowSize<RefAndTwoInt32Wrappers>();
    }

    static void ShowSize<T>()
    {
        long before = GC.GetTotalMemory(true);
        T[] array = new T[100000];
        long after  = GC.GetTotalMemory(true);        
        Console.WriteLine("{0}: {1}", typeof(T),
                          (after - before) / array.Length);
    }
}

Và quá trình biên dịch và xuất trên máy tính xách tay của tôi:

c:\Users\Jon\Test>csc /debug- /o+ ShowMemory.cs
Microsoft (R) Visual C# Compiler version 12.0.30501.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.


c:\Users\Jon\Test>ShowMemory.exe
Environment: CLR 4.0.30319.34014 on Microsoft Windows NT 6.2.9200.0 (64 bit)
Int32Wrapper: 4
TwoInt32s: 8
TwoInt32Wrappers: 8
RefAndTwoInt32s: 16
RefAndTwoInt32Wrappers: 24

Vì thế:

  • Nếu bạn không có trường loại tham chiếu, CLR rất vui khi đóng gói Int32Wrappercác trường lại với nhau ( TwoInt32Wrapperscó kích thước 8)
  • Ngay cả với trường kiểu tham chiếu, CLR vẫn rất vui khi đóng gói intcác trường lại với nhau ( RefAndTwoInt32scó kích thước là 16)
  • Kết hợp cả hai, mỗi Int32Wrappertrường dường như được đệm / căn chỉnh thành 8 byte. ( RefAndTwoInt32Wrapperscó kích thước là 24.)
  • Chạy cùng một mã trong trình gỡ lỗi (nhưng vẫn là một bản dựng phát hành) sẽ hiển thị kích thước là 12.

Một số thí nghiệm khác cũng cho kết quả tương tự:

  • Đặt trường loại tham chiếu sau các trường loại giá trị không hữu ích
  • Sử dụng objectthay vì stringkhông hữu ích (tôi mong đợi đó là "bất kỳ loại tham chiếu nào")
  • Sử dụng một cấu trúc khác làm "trình bao bọc" xung quanh tham chiếu không hữu ích
  • Sử dụng cấu trúc chung chung làm trình bao bọc xung quanh tham chiếu không hữu ích
  • Nếu tôi tiếp tục thêm các trường (theo cặp cho đơn giản), intcác trường vẫn được tính là 4 byte và Int32Wrappercác trường được tính là 8 byte
  • Thêm [StructLayout(LayoutKind.Sequential, Pack = 4)]vào mọi cấu trúc trong tầm nhìn không thay đổi kết quả

Có ai có bất kỳ lời giải thích nào cho điều này (lý tưởng là với tài liệu tham khảo) hoặc gợi ý về cách tôi có thể nhận được gợi ý về CLR mà tôi muốn các trường được đóng gói mà không chỉ định độ lệch trường không đổi?


1
Bạn dường như không thực sự đang sử dụng Ref<T>nhưng đang sử dụng stringthay thế, không phải là nó sẽ tạo ra sự khác biệt.
tvanfosson

2
Điều gì xảy ra nếu bạn đặt hai và tạo một cấu trúc với hai TwoInt32Wrappershoặc một Int64và một TwoInt32Wrappers? Còn nếu bạn tạo chung chung Pair<T1,T2> {public T1 f1; public T2 f2;}rồi tạo Pair<string,Pair<int,int>>Pair<string,Pair<Int32Wrapper,Int32Wrapper>>? Những kết hợp nào buộc JITter phải độn mọi thứ?
supercat

7
@supercat: Đây có thể là tốt nhất cho bạn để sao chép mã và thử nghiệm cho chính mình - nhưng Pair<string, TwoInt32Wrappers> không đưa ra chỉ 16 byte, do đó sẽ giải quyết vấn đề. Hấp dẫn.
Jon Skeet

9
@SLaks: Đôi khi khi một cấu trúc được chuyển sang mã gốc, Runtime sẽ sao chép tất cả dữ liệu vào một cấu trúc có bố cục khác. Marshal.SizeOfsẽ trả về kích thước của cấu trúc sẽ được chuyển cho mã gốc, không cần có bất kỳ mối liên hệ nào với kích thước của cấu trúc trong mã .NET.
supercat

5
Quan sát thú vị: Mono cho kết quả chính xác. Môi trường: CLR 4.0.30319.17020 trên Unix 3.13.0.24 (64 bit) Int32Wrapper: 4 TwoInt32s: 8 TwoInt32Wrappers: 8 RefAndTwoInt32s: 16 RefAndTwoInt32Wrappers: 16
AndreyAkinshin

Câu trả lời:


85

Tôi nghi no la một con bọ. Bạn đang thấy tác dụng phụ của bố cục tự động, nó thích sắp xếp các trường không tầm thường với một địa chỉ là bội số của 8 byte ở chế độ 64 bit. Nó xảy ra ngay cả khi bạn áp dụng [StructLayout(LayoutKind.Sequential)]thuộc tính một cách rõ ràng . Điều đó không được cho là xảy ra.

Bạn có thể thấy điều đó bằng cách đặt thành viên cấu trúc ở chế độ công khai và thêm mã kiểm tra như sau:

    var test = new RefAndTwoInt32Wrappers();
    test.text = "adsf";
    test.x.x = 0x11111111;
    test.y.x = 0x22222222;
    Console.ReadLine();      // <=== Breakpoint here

Khi chạm đến điểm ngắt, hãy sử dụng Gỡ lỗi + Windows + Bộ nhớ + Bộ nhớ 1. Chuyển sang số nguyên 4 byte và đặt &testvào trường Địa chỉ:

 0x000000E928B5DE98  0ed750e0 000000e9 11111111 00000000 22222222 00000000 

0xe90ed750e0là con trỏ chuỗi trên máy của tôi (không phải của bạn). Bạn có thể dễ dàng nhìn thấy Int32Wrappers, với 4 byte đệm bổ sung đã biến kích thước thành 24 byte. Quay lại cấu trúc và đặt chuỗi cuối cùng. Lặp lại và bạn sẽ thấy con trỏ chuỗi vẫn ở đầu tiên. Vi phạm LayoutKind.Sequential, bạn có LayoutKind.Auto.

Sẽ rất khó để thuyết phục Microsoft sửa lỗi này, nó đã hoạt động theo cách này quá lâu nên bất kỳ thay đổi nào cũng có thể phá vỡ điều gì đó . CLR chỉ cố gắng tôn vinh[StructLayout] phiên bản được quản lý của một cấu trúc và làm cho nó trở nên dễ hiểu, nói chung nó nhanh chóng bỏ cuộc. Nổi tiếng đối với bất kỳ cấu trúc nào chứa DateTime. Bạn chỉ nhận được đảm bảo LayoutKind thực sự khi sắp xếp một cấu trúc. Phiên bản thống nhất chắc chắn là 16 byte, như Marshal.SizeOf()sẽ cho bạn biết.

Sử dụng các LayoutKind.Explicitbản sửa lỗi, không phải những gì bạn muốn nghe.


7
"Sẽ rất khó để thuyết phục Microsoft sửa lỗi này, nó đã hoạt động theo cách này quá lâu nên bất kỳ thay đổi nào cũng sẽ phá vỡ điều gì đó." Thực tế là điều này dường như không hiển thị trong 32 bit hoặc mono có thể giúp ích (theo các nhận xét khác).
NPSF3000,

Tài liệu của StructLayoutAttribute khá thú vị. Về cơ bản, chỉ các loại blittable được kiểm soát thông qua StructLayout trong bộ nhớ được quản lý. Thật thú vị, không bao giờ biết rằng.
Michael Stum

@Soner không nó không sửa nó. Bạn đã đặt Bố cục trên cả hai trường để được bù trừ 8? Nếu vậy thì x và y giống nhau và thay đổi một sẽ thay đổi khác. Rõ ràng không phải thứ Jon đang theo đuổi.
BartoszAdamczewski

Việc thay thế stringbằng một kiểu tham chiếu mới khác ( class) mà kiểu tham chiếu đã áp dụng [StructLayout(LayoutKind.Sequential)]dường như không thay đổi bất cứ điều gì. Theo hướng ngược lại, áp dụng [StructLayout(LayoutKind.Auto)]cho các struct Int32Wrapperthay đổi sử dụng bộ nhớ trong TwoInt32Wrappers.
Jeppe Stig Nielsen,

1
"Sẽ rất khó để thuyết phục Microsoft sửa lỗi này, nó đã hoạt động theo cách này quá lâu nên bất kỳ thay đổi nào cũng sẽ phá vỡ điều gì đó." xkcd.com/1172
iCodeSometime

19

EDIT2

struct RefAndTwoInt32Wrappers
{
    public int x;
    public string s;
}

Mã này sẽ được căn chỉnh 8 byte nên cấu trúc sẽ có 16 byte. Bằng cách so sánh này:

struct RefAndTwoInt32Wrappers
{
    public int x,y;
    public string s;
}

Sẽ được căn chỉnh 4 byte nên cấu trúc này cũng sẽ có 16 byte. Vì vậy, lý do ở đây là việc sắp xếp cấu trúc trong CLR được xác định bởi số lượng các trường được căn chỉnh nhiều nhất, các clases rõ ràng không thể làm điều đó nên chúng sẽ vẫn được căn chỉnh 8 byte.

Bây giờ nếu chúng ta kết hợp tất cả những thứ đó và tạo ra struct:

struct RefAndTwoInt32Wrappers
{
    public int x,y;
    public Int32Wrapper z;
    public string s;
}

Nó sẽ có 24 byte {x, y} sẽ có 4 byte mỗi byte và {z, s} sẽ có 8 byte. Khi chúng tôi giới thiệu một loại tham chiếu trong struct CLR sẽ luôn căn chỉnh cấu trúc tùy chỉnh của chúng tôi để phù hợp với căn chỉnh lớp.

struct RefAndTwoInt32Wrappers
{
    public Int32Wrapper z;
    public long l;
    public int x,y;  
}

Mã này sẽ có 24 byte vì Int32Wrapper sẽ được căn chỉnh giống nhau. Vì vậy, trình bao bọc cấu trúc tùy chỉnh sẽ luôn căn chỉnh với trường được căn chỉnh cao nhất / tốt nhất trong cấu trúc hoặc với các trường quan trọng nhất bên trong của chính nó. Vì vậy, trong trường hợp một chuỗi ref có 8 byte được căn chỉnh, trình bao bọc cấu trúc sẽ căn chỉnh theo đó.

Trường cấu trúc tùy chỉnh kết hợp bên trong cấu trúc sẽ luôn được căn chỉnh với trường phiên bản được căn chỉnh cao nhất trong cấu trúc. Bây giờ nếu tôi không chắc liệu đây có phải là một lỗi hay không nhưng nếu không có một số bằng chứng, tôi sẽ theo quan điểm của mình rằng đây có thể là quyết định có ý thức.


BIÊN TẬP

Các kích thước thực sự chỉ chính xác khi được phân bổ trên một đống nhưng bản thân các cấu trúc có kích thước nhỏ hơn (kích thước chính xác của các trường đó). Đường phân tích sâu hơn để gợi ý rằng đây có thể là một lỗi trong mã CLR, nhưng cần được sao lưu bằng bằng chứng.

Tôi sẽ kiểm tra mã cli và đăng thêm các bản cập nhật nếu có điều gì hữu ích sẽ được tìm thấy.


Đây là một chiến lược căn chỉnh được sử dụng bởi trình cấp phát mem .NET.

public static RefAndTwoInt32s[] test = new RefAndTwoInt32s[1];

static void Main()
{
    test[0].text = "a";
    test[0].x = 1;
    test[0].x = 1;

    Console.ReadKey();
}

Mã này được biên dịch với .net40 theo x64, Trong WinDbg, hãy thực hiện những việc sau:

Trước tiên hãy tìm kiểu trên Heap:

    0:004> !dumpheap -type Ref
       Address               MT     Size
0000000003e72c78 000007fe61e8fb58       56    
0000000003e72d08 000007fe039d3b78       40    

Statistics:
              MT    Count    TotalSize Class Name
000007fe039d3b78        1           40 RefAndTwoInt32s[]
000007fe61e8fb58        1           56 System.Reflection.RuntimeAssembly
Total 2 objects

Khi chúng tôi có nó, hãy xem những gì dưới địa chỉ đó:

    0:004> !do 0000000003e72d08
Name:        RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass:     000007fe039d3ad0
Size:        40(0x28) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Fields:
None

Chúng tôi thấy rằng đây là một ValueType và là một trong những chúng tôi đã tạo. Vì đây là một mảng nên chúng ta cần lấy định nghĩa ValueType của một phần tử duy nhất trong mảng:

    0:004> !dumparray -details 0000000003e72d08
Name:        RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass:     000007fe039d3ad0
Size:        40(0x28) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3a58
[0] 0000000003e72d18
    Name:        RefAndTwoInt32s
    MethodTable: 000007fe039d3a58
    EEClass:     000007fe03ae2338
    Size:        32(0x20) bytes
    File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        000007fe61e8c358  4000006        0            System.String      0     instance     0000000003e72d30     text
        000007fe61e8f108  4000007        8             System.Int32      1     instance                    1     x
        000007fe61e8f108  4000008        c             System.Int32      1     instance                    0     y

Cấu trúc thực sự là 32 byte vì nó 16 byte được dành riêng cho phần đệm nên trên thực tế, mọi cấu trúc có kích thước ít nhất là 16 byte tính từ đầu.

nếu bạn thêm 16 byte từ int và một tham chiếu chuỗi vào: 0000000003e72d18 + 8 byte EE / padding, bạn sẽ kết thúc ở 0000000003e72d30 và đây là điểm bắt đầu cho tham chiếu chuỗi và vì tất cả các tham chiếu đều được đệm 8 byte từ trường dữ liệu thực đầu tiên của chúng điều này tạo nên 32 byte của chúng ta cho cấu trúc này.

Hãy xem liệu chuỗi có thực sự được đệm theo cách đó không:

0:004> !do 0000000003e72d30    
Name:        System.String
MethodTable: 000007fe61e8c358
EEClass:     000007fe617f3720
Size:        28(0x1c) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      a
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fe61e8f108  40000aa        8         System.Int32  1 instance                1 m_stringLength
000007fe61e8d640  40000ab        c          System.Char  1 instance               61 m_firstChar
000007fe61e8c358  40000ac       18        System.String  0   shared           static Empty
                                 >> Domain:Value  0000000001577e90:NotInit  <<

Bây giờ chúng ta hãy phân tích chương trình trên theo cùng một cách:

public static RefAndTwoInt32Wrappers[] test = new RefAndTwoInt32Wrappers[1];

static void Main()
{
    test[0].text = "a";
    test[0].x.x = 1;
    test[0].y.x = 1;

    Console.ReadKey();
}

0:004> !dumpheap -type Ref
     Address               MT     Size
0000000003c22c78 000007fe61e8fb58       56    
0000000003c22d08 000007fe039d3c00       48    

Statistics:
              MT    Count    TotalSize Class Name
000007fe039d3c00        1           48 RefAndTwoInt32Wrappers[]
000007fe61e8fb58        1           56 System.Reflection.RuntimeAssembly
Total 2 objects

Cấu trúc của chúng tôi bây giờ là 48 byte.

0:004> !dumparray -details 0000000003c22d08
Name:        RefAndTwoInt32Wrappers[]
MethodTable: 000007fe039d3c00
EEClass:     000007fe039d3b58
Size:        48(0x30) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3ae0
[0] 0000000003c22d18
    Name:        RefAndTwoInt32Wrappers
    MethodTable: 000007fe039d3ae0
    EEClass:     000007fe03ae2338
    Size:        40(0x28) bytes
    File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        000007fe61e8c358  4000009        0            System.String      0     instance     0000000003c22d38     text
        000007fe039d3a20  400000a        8             Int32Wrapper      1     instance     0000000003c22d20     x
        000007fe039d3a20  400000b       10             Int32Wrapper      1     instance     0000000003c22d28     y

Ở đây tình huống cũng tương tự, nếu chúng ta thêm vào 0000000003c22d18 + 8 byte chuỗi ref, chúng ta sẽ kết thúc ở đầu trình bao bọc Int đầu tiên nơi giá trị thực sự trỏ đến địa chỉ chúng ta đang ở.

Bây giờ chúng ta có thể thấy rằng mỗi giá trị là một tham chiếu đối tượng một lần nữa, hãy xác nhận điều đó bằng cách xem qua 0000000003c22d20.

0:004> !do 0000000003c22d20
<Note: this object has an invalid CLASS field>
Invalid object

Trên thực tế, điều đó đúng vì địa chỉ của nó là một cấu trúc, địa chỉ không cho chúng ta biết gì nếu đây là một obj hoặc vt.

0:004> !dumpvc 000007fe039d3a20   0000000003c22d20    
Name:        Int32Wrapper
MethodTable: 000007fe039d3a20
EEClass:     000007fe03ae23c8
Size:        24(0x18) bytes
File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fe61e8f108  4000001        0         System.Int32  1 instance                1 x

Vì vậy, trên thực tế, đây là một kiểu Liên minh giống như kiểu Union sẽ được căn chỉnh 8 byte vào khoảng thời gian này (tất cả các phần đệm sẽ được căn chỉnh với cấu trúc mẹ). Nếu không phải thì chúng ta sẽ có 20 byte và đó không phải là tối ưu nên trình cấp phát mem sẽ không bao giờ cho phép điều đó xảy ra. Nếu bạn thực hiện lại phép toán, bạn sẽ thấy rằng cấu trúc thực sự có kích thước 40 byte.

Vì vậy, nếu bạn muốn tiết kiệm hơn với bộ nhớ, bạn không nên đóng gói nó theo kiểu cấu trúc tùy chỉnh struct mà thay vào đó hãy sử dụng các mảng đơn giản. Một cách khác là cấp phát bộ nhớ khỏi heap (ví dụ: VirtualAllocEx) theo cách này, bạn được cấp khối bộ nhớ riêng và bạn quản lý nó theo cách bạn muốn.

Câu hỏi cuối cùng ở đây là tại sao đột nhiên chúng ta lại có được bố cục như vậy. Vâng, nếu bạn so sánh mã jited và hiệu suất của phần tăng int [] với struct [] với phần tăng của trường bộ đếm, cái thứ hai sẽ tạo ra một địa chỉ được căn chỉnh 8 byte là một liên hợp, nhưng khi jited điều này sẽ chuyển thành mã hợp ngữ được tối ưu hóa hơn (singe LEA so với nhiều MOV). Tuy nhiên, trong trường hợp được mô tả ở đây, hiệu suất sẽ thực sự kém hơn vì vậy quan điểm của tôi là điều này phù hợp với việc triển khai CLR cơ bản vì nó là một loại tùy chỉnh có thể có nhiều trường nên có thể dễ dàng / tốt hơn để đặt địa chỉ bắt đầu thay vì giá trị (vì nó sẽ là không thể) và đệm cấu trúc ở đó, do đó dẫn đến kích thước byte lớn hơn.


1
Bản thân tôi nhìn vào điều này, kích thước RefAndTwoInt32Wrappers không phải là 32 byte - mà là 24, giống như được báo cáo với mã của tôi. Nếu bạn nhìn trong chế độ xem bộ nhớ thay vì sử dụng dumparrayvà nhìn vào bộ nhớ cho một mảng có (giả sử) 3 phần tử với các giá trị có thể phân biệt, bạn có thể thấy rõ rằng mỗi phần tử bao gồm một tham chiếu chuỗi 8 byte và hai số nguyên 8 byte . Tôi nghi ngờ điều đó dumparrayđang hiển thị các giá trị dưới dạng tham chiếu đơn giản vì nó không biết cách hiển thị Int32Wrappergiá trị. Những "tham chiếu" đó chỉ về bản thân họ; chúng không phải là các giá trị riêng biệt.
Jon Skeet

1
Tôi không chắc bạn lấy "phần đệm 16 byte" từ đâu, nhưng tôi nghi ngờ có thể là do bạn đang xem kích thước của đối tượng mảng, sẽ là "16 byte + số lượng * kích thước phần tử". Vì vậy, một mảng có số đếm 2 có kích thước là 72 (16 + 2 * 24), đó là những gì dumparrayhiển thị.
Jon Skeet

@jon bạn đã kết xuất cấu trúc của mình và kiểm tra xem nó chiếm bao nhiêu dung lượng trên heap chưa? Thông thường kích thước mảng được giữ ở đầu mảng, điều này cũng có thể được xác minh.
BartoszAdamczewski

@jon kích thước được báo cáo cũng chứa phần bù của chuỗi bắt đầu từ 8. Tôi không nghĩ rằng 8 byte bổ sung đó được đề cập đến từ mảng vì hầu hết nội dung mảng nằm trước địa chỉ phần tử đầu tiên, nhưng tôi sẽ kiểm tra kỹ và bình luận về điều đó.
BartoszAdamczewski

1
Không, ThreeInt32Wrappers kết thúc là 12 byte, FourInt32Wrappers là 16, FiveInt32Wrappers là 20. Tôi không thấy bất kỳ điều gì hợp lý về việc thêm trường loại tham chiếu làm thay đổi bố cục đáng kể. Và lưu ý rằng khá vui khi bỏ qua căn chỉnh 8 byte khi các trường thuộc loạiInt32 . Tôi không quá bận tâm về những gì nó làm trên ngăn xếp, thành thật mà nói - nhưng tôi chưa kiểm tra nó.
Jon Skeet

9

Tóm tắt, hãy xem câu trả lời của @Hans Passant có lẽ ở trên. Tuần tự bố cục không hoạt động


Một số thử nghiệm:

Nó chắc chắn chỉ trên 64bit và đối tượng tham chiếu "đầu độc" struct. 32 bit thực hiện những gì bạn đang mong đợi:

Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (32 bit)
ConsoleApplication1.Int32Wrapper: 4
ConsoleApplication1.TwoInt32s: 8
ConsoleApplication1.TwoInt32Wrappers: 8
ConsoleApplication1.ThreeInt32Wrappers: 12
ConsoleApplication1.Ref: 4
ConsoleApplication1.RefAndTwoInt32s: 12
ConsoleApplication1.RefAndTwoInt32Wrappers: 12
ConsoleApplication1.RefAndThreeInt32s: 16
ConsoleApplication1.RefAndThreeInt32Wrappers: 16

Ngay sau khi tham chiếu đối tượng được thêm vào, tất cả các cấu trúc sẽ mở rộng thành 8 byte thay vì kích thước 4 byte của chúng. Mở rộng các bài kiểm tra:

Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (64 bit)
ConsoleApplication1.Int32Wrapper: 4
ConsoleApplication1.TwoInt32s: 8
ConsoleApplication1.TwoInt32Wrappers: 8
ConsoleApplication1.ThreeInt32Wrappers: 12
ConsoleApplication1.Ref: 8
ConsoleApplication1.RefAndTwoInt32s: 16
ConsoleApplication1.RefAndTwoInt32sSequential: 16
ConsoleApplication1.RefAndTwoInt32Wrappers: 24
ConsoleApplication1.RefAndThreeInt32s: 24
ConsoleApplication1.RefAndThreeInt32Wrappers: 32
ConsoleApplication1.RefAndFourInt32s: 24
ConsoleApplication1.RefAndFourInt32Wrappers: 40

Như bạn có thể thấy ngay sau khi tham chiếu được thêm vào, mỗi Int32Wrapper sẽ trở thành 8 byte, vì vậy việc căn chỉnh không đơn giản. Tôi đã thu nhỏ phân bổ mảng trong trường hợp phân bổ LoH được căn chỉnh khác nhau.


4

Chỉ để thêm một số dữ liệu vào hỗn hợp - tôi đã tạo thêm một loại từ những loại bạn có:

struct RefAndTwoInt32Wrappers2
{
    string text;
    TwoInt32Wrappers z;
}

Chương trình viết ra:

RefAndTwoInt32Wrappers2: 16

Vì vậy, có vẻ như TwoInt32Wrapperscấu trúc sắp xếp đúng trong RefAndTwoInt32Wrappers2cấu trúc mới .


Bạn đang chạy 64 bit? Liên kết là tốt trong 32 bit
Ben Adams

Những phát hiện của tôi cũng giống như mọi người đối với các môi trường khác nhau.
Jesse C. Slicer,
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.