Dlaczego wyrównanie struktury zależy od tego, czy typ pola jest pierwotny, czy zdefiniowany przez użytkownika?

121

W Noda Time v2 przechodzimy do rozdzielczości nanosekundowej. Oznacza to, że nie możemy już używać 8-bajtowej liczby całkowitej do reprezentowania całego zakresu czasu, który nas interesuje. To skłoniło mnie do zbadania wykorzystania pamięci przez (wiele) struktur czasu Noda, co z kolei doprowadziło mnie do aby odkryć niewielką dziwność w decyzji dotyczącej wyrównania CLR.

Po pierwsze, zdaję sobie sprawę, że jest to decyzja dotycząca wdrożenia i że domyślne zachowanie może się zmienić w dowolnym momencie. Zdaję sobie sprawę, że mogę to zmodyfikować za pomocą[StructLayout] i [FieldOffset], ale wolałbym wymyślić rozwiązanie, które nie wymagałoby tego, jeśli to możliwe.

Mój podstawowy scenariusz jest taki, że mam pole, structktóre zawiera pole typu referencyjnego i dwa inne pola typu wartości, gdzie te pola są prostymi opakowaniami dlaint . Miałem nadzieję , że będzie to reprezentowane jako 16 bajtów w 64-bitowym CLR (8 dla odniesienia i 4 dla każdego z pozostałych), ale z jakiegoś powodu używa 24 bajtów. Nawiasem mówiąc, mierzę przestrzeń za pomocą tablic - rozumiem, że układ może być inny w różnych sytuacjach, ale wydawało się to rozsądnym punktem wyjścia.

Oto przykładowy program demonstrujący problem:

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);
    }
}

A kompilacja i wyjście na moim laptopie:

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

Więc:

  • Jeśli nie masz pola typu referencyjnego, CLR z przyjemnością spakuje Int32Wrapperpola razem ( TwoInt32Wrappersma rozmiar 8)
  • Nawet z polem typu referencyjnego CLR nadal jest zadowolony z pakowania int pola razem ( RefAndTwoInt32sma rozmiar 16)
  • Łącząc oba, każde Int32Wrapperpole wydaje się być wypełnione / wyrównane do 8 bajtów. (RefAndTwoInt32Wrappers ma rozmiar 24.)
  • Uruchomienie tego samego kodu w debugerze (ale nadal kompilacji wydania) pokazuje rozmiar 12.

Kilka innych eksperymentów przyniosło podobne wyniki:

  • Umieszczenie pola typu odwołania po polach typu wartości nie pomaga
  • Używanie objectzamiaststring nie pomaga (spodziewam się, że jest to „dowolny typ referencyjny”)
  • Używanie innej struktury jako „opakowania” wokół referencji nie pomaga
  • Używanie ogólnej struktury jako opakowania wokół odwołania nie pomaga
  • Jeśli będę nadal dodawać pola (w parach dla uproszczenia), intpola nadal liczą się na 4 bajty, aInt32Wrapper pola liczą się do 8 bajtów
  • Dodanie [StructLayout(LayoutKind.Sequential, Pack = 4)]do każdej struktury w zasięgu wzroku nie zmienia wyników

Czy ktoś ma na to jakieś wyjaśnienie (najlepiej z dokumentacją referencyjną) lub sugestię, jak mogę uzyskać wskazówkę do CLR, że chciałbym, aby pola były pakowane bez określania stałego przesunięcia pól?

Jon Skeet
źródło
1
Wydaje się, że nie używasz, Ref<T>ale stringzamiast tego używasz , nie żeby to miało coś zmienić.
tvanfosson
2
Co się stanie, jeśli umieścisz dwa i utworzysz strukturę z dwoma TwoInt32Wrapperslub an Int64i a TwoInt32Wrappers? A co jeśli utworzysz ogólny, Pair<T1,T2> {public T1 f1; public T2 f2;}a następnie utworzysz Pair<string,Pair<int,int>>i Pair<string,Pair<Int32Wrapper,Int32Wrapper>>? Jakie kombinacje zmuszają JITter do wypełniania elementów?
supercat
7
@supercat: To chyba najlepiej, aby skopiować kod i eksperyment dla siebie - ale Pair<string, TwoInt32Wrappers> nie daje zaledwie 16 bajtów, tak by rozwiązać problem. Fascynujący.
Jon Skeet
9
@SLaks: Czasami, gdy struktura jest przekazywana do kodu natywnego, środowisko wykonawcze skopiuje wszystkie dane do struktury o innym układzie. Marshal.SizeOfzwróci rozmiar struktury, która zostanie przekazana do kodu natywnego, który nie musi mieć żadnego związku z rozmiarem struktury w kodzie .NET.
supercat
5
Ciekawe spostrzeżenie: Mono daje prawidłowe wyniki. Środowisko: CLR 4.0.30319.17020 na Unix 3.13.0.24 (64 bit) Int32Wrapper: 4 TwoInt32s: 8 TwoInt32Wrappers: 8 RefAndTwoInt32s: 16 RefAndTwoInt32Wrappers: 16
AndreyAkinshin

Odpowiedzi:

85

Myślę, że to błąd. Widzisz efekt uboczny automatycznego układu, lubi wyrównywać nietrywialne pola do adresu, który jest wielokrotnością 8 bajtów w trybie 64-bitowym. Występuje nawet wtedy, gdy jawnie zastosujesz[StructLayout(LayoutKind.Sequential)] atrybut. To nie powinno się wydarzyć.

Możesz to zobaczyć, upubliczniając członków struktury i dołączając kod testowy w następujący sposób:

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

Kiedy punkt przerwania trafi, użyj Debug + Windows + Memory + Memory 1. Przełącz na 4-bajtowe liczby całkowite i wpisz &testw polu Adres:

 0x000000E928B5DE98  0ed750e0 000000e9 11111111 00000000 22222222 00000000 

0xe90ed750e0jest wskaźnikiem ciągu na mojej maszynie (nie na twoim). Możesz łatwo zobaczyć Int32Wrappers, z dodatkowymi 4 bajtami wypełnienia, które zmieniły rozmiar na 24 bajty. Wróć do struktury i umieść strunę jako ostatnią. Powtórz, a zobaczysz, że wskaźnik ciągu jest nadal pierwszy. Naruszasz LayoutKind.Sequential, maszLayoutKind.Auto .

Trudno będzie przekonać Microsoft, aby to naprawił, działało w ten sposób zbyt długo, więc każda zmiana może coś zepsuć . CLR tylko próbuje uhonorować [StructLayout]zarządzaną wersję struktury i uczynić ją zapisywalną, ogólnie szybko się poddaje. Notorycznie dla każdej struktury, która zawiera DateTime. Otrzymujesz tylko prawdziwą gwarancję LayoutKind podczas organizowania struktury. Jak powiesz, wersja zorganizowana ma z pewnością 16 bajtów Marshal.SizeOf().

Użycie LayoutKind.Explicitrozwiązuje problem, a nie to, co chciałeś usłyszeć.

Hans Passant
źródło
7
„Trudno będzie przekonać Microsoft, aby to naprawił, działało w ten sposób zbyt długo, więc każda zmiana może coś zepsuć”. Fakt, że najwyraźniej nie występuje to w wersji 32-bitowej lub monofonicznej, może pomóc (jak w innych komentarzach).
NPSF3000
Dokumentacja StructLayoutAttribute jest całkiem interesująca. Zasadniczo tylko typy kopiowalne są kontrolowane przez StructLayout w pamięci zarządzanej. Ciekawe, nigdy tego nie wiedziałem.
Michael Stum
@Soner no, to nie naprawia tego. Czy umieściłeś układ na obu polach, aby uzyskać przesunięcie 8? Jeśli tak, to x i y są takie same, a zmiana jednego powoduje zmianę drugiego. Najwyraźniej nie to, czego szuka Jon.
BartoszAdamczewski 15.07.14
Zastąpienie stringinnym nowym typem odniesienia ( class), do którego zastosowano [StructLayout(LayoutKind.Sequential)], nie wydaje się niczego zmieniać. W przeciwnym kierunku, przy zastosowaniu [StructLayout(LayoutKind.Auto)]do struct Int32Wrapperzmiany zużycia pamięci w TwoInt32Wrappers.
Jeppe Stig Nielsen
1
„Trudno będzie przekonać Microsoft, aby to naprawił, działało w ten sposób zbyt długo, więc każda zmiana może coś zepsuć”. xkcd.com/1172
iCodeSometime
19

EDYCJA2

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

Ten kod będzie wyrównany do 8 bajtów, więc struktura będzie miała 16 bajtów. Dla porównania to:

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

Będzie wyrównany do 4 bajtów, więc ta struktura również będzie miała 16 bajtów. Zatem uzasadnienie jest takie, że wyrównanie struktury w CLR jest określane przez liczbę najbardziej wyrównanych pól, klasy oczywiście nie mogą tego zrobić, więc pozostaną wyrównane 8 bajtów.

Teraz, jeśli połączymy to wszystko i stworzymy strukturę:

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

Będzie miał 24 bajty {x, y} będzie miał 4 bajty każdy, a {z, s} będzie miał 8 bajtów. Po wprowadzeniu typu ref w strukturze CLR zawsze wyrówna naszą niestandardową strukturę, aby pasowała do wyrównania klasy.

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

Ten kod będzie miał 24 bajty, ponieważ Int32Wrapper zostanie wyrównany tak samo jak long. Tak więc opakowanie struktury niestandardowej będzie zawsze wyrównywane do najwyższego / najlepiej wyrównanego pola w strukturze lub do własnych wewnętrznych najbardziej znaczących pól. Tak więc w przypadku łańcucha ref, który jest wyrównany do 8 bajtów, opakowanie struktury zostanie wyrównane do tego.

Zamknięcie pola niestandardowej struktury wewnątrz struktury będzie zawsze wyrównane do najwyższego wyrównanego pola wystąpienia w strukturze. Teraz, jeśli nie jestem pewien, czy to błąd, ale bez pewnych dowodów, będę trzymać się mojej opinii, że może to być świadoma decyzja.


EDYTOWAĆ

Rozmiary są w rzeczywistości dokładne tylko wtedy, gdy są przydzielane na stercie, ale same struktury mają mniejsze rozmiary (dokładne rozmiary ich pól). Dalsza analiza sugeruje, że może to być błąd w kodzie CLR, ale musi być poparty dowodami.

Sprawdzę kod CLI i opublikuję dalsze aktualizacje, jeśli znajdzie się coś przydatnego.


Jest to strategia dopasowania używana przez alokator pamięci .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();
}

Ten kod skompilowany z .net40 pod x64, w WinDbg wykonaj następujące czynności:

Najpierw znajdźmy typ na stercie:

    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

Kiedy już to zrobimy, zobaczmy, co jest pod tym adresem:

    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

Widzimy, że jest to ValueType i to jest ten, który stworzyliśmy. Ponieważ jest to tablica, musimy pobrać wartość ValueType pojedynczego elementu w tablicy:

    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

Struktura ma w rzeczywistości 32 bajty, ponieważ 16 bajtów jest zarezerwowanych na wypełnienie, więc w rzeczywistości każda struktura ma rozmiar co najmniej 16 bajtów od momentu rozpoczęcia.

jeśli dodasz 16 bajtów z ints i ciąg ref do: 0000000003e72d18 + 8 bajtów EE / dopełnienie, skończysz na 0000000003e72d30 i jest to punkt początkowy dla odniesienia do łańcucha, a ponieważ wszystkie odwołania są wypełnione 8 bajtami od ich pierwszego rzeczywistego pola danych to daje nam 32 bajty dla tej struktury.

Zobaczmy, czy ciąg jest rzeczywiście wypełniony w ten sposób:

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  <<

Teraz przeanalizujmy powyższy program w ten sam sposób:

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

Nasza struktura ma teraz 48 bajtów.

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

Tutaj sytuacja jest taka sama, jeśli dodamy do 0000000003c22d18 + 8 bajtów string ref, skończymy na początku pierwszego opakowania Int, gdzie wartość faktycznie wskazuje na adres, pod którym się znajdujemy.

Teraz widzimy, że każda wartość jest odwołaniem do obiektu, ponownie potwierdźmy to, sprawdzając 0000000003c22d20.

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

Właściwie to jest poprawne, ponieważ jest to struktura a adres nie mówi nam nic, jeśli jest to obj lub 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

W rzeczywistości jest to bardziej podobny do typu Union, w którym tym razem zostanie wyrównane 8 bajtów (wszystkie dopełnienia zostaną wyrównane ze strukturą nadrzędną). Gdyby tak nie było, otrzymalibyśmy 20 bajtów, a to nie jest optymalne, więc alokator pamięci nigdy na to nie pozwoli. Jeśli ponownie wykonasz obliczenia, okaże się, że struktura ma rzeczywiście 40 bajtów.

Więc jeśli chcesz być bardziej konserwatywny w kwestii pamięci, nigdy nie powinieneś pakować jej w niestandardowy typ struktury, ale zamiast tego używać prostych tablic. Innym sposobem jest alokacja pamięci poza stertą (np. VirtualAllocEx) w ten sposób, że otrzymasz własny blok pamięci i będziesz nim zarządzać tak, jak chcesz.

Ostatnie pytanie brzmi: dlaczego nagle możemy otrzymać taki układ. Cóż, jeśli porównasz jited kod i wydajność inkrementacji int [] ze strukturą struct [] z inkrementacją pola licznika, druga wygeneruje 8-bajtowy adres wyrównany będący sumą, ale kiedy jited to przekłada się na bardziej zoptymalizowany kod asemblera (singe LEA vs wiele plików MOV). Jednak w opisanym tutaj przypadku wydajność będzie faktycznie gorsza, więc moim zdaniem jest to zgodne z podstawową implementacją CLR, ponieważ jest to typ niestandardowy, który może mieć wiele pól, więc może być łatwiej / lepiej umieścić adres początkowy zamiast value (ponieważ byłoby to niemożliwe) i wykonaj tam dopełnienie struktury, co spowoduje większy rozmiar bajtu.

BartoszAdamczewski
źródło
1
Patrząc na to osobiście, rozmiar RefAndTwoInt32Wrappers nie wynosi 32 bajty - to 24, czyli to samo, co zgłoszono w moim kodzie. Jeśli spojrzysz na widok pamięci zamiast używać dumparrayi spojrzysz na pamięć dla tablicy z (powiedzmy) 3 elementami z rozróżnialnymi wartościami, możesz wyraźnie zobaczyć, że każdy element składa się z 8-bajtowego odwołania do łańcucha i dwóch 8-bajtowych liczb całkowitych . Podejrzewam, że dumparraypokazuje wartości jako odniesienia tylko dlatego, że nie wie, jak wyświetlić Int32Wrapperwartości. Te „odniesienia” wskazują na siebie; nie są to oddzielne wartości.
Jon Skeet,
1
Nie jestem do końca pewien, skąd bierze się „wypełnienie 16 bajtów”, ale podejrzewam, że może to wynikać z tego, że patrzysz na rozmiar obiektu tablicy, który wyniesie „16 bajtów + liczba * rozmiar elementu”. A więc tablica z liczbą 2 ma rozmiar 72 (16 + 2 * 24), co dumparraypokazuje.
Jon Skeet,
@jon czy zrzuciłeś swoją strukturę i sprawdziłeś, ile miejsca zajmuje ona na stercie? Zwykle rozmiar tablicy jest przechowywany na początku tablicy, można to również zweryfikować.
BartoszAdamczewski 15.07.14
@jon, podany rozmiar zawiera również przesunięcie ciągu, który zaczyna się od 8. Nie sądzę, aby te dodatkowe 8 bajtów, o których mowa, pochodziło z tablicy, ponieważ większość elementów tablicy znajduje się przed adresem pierwszego elementu, ale sprawdzę dokładnie i skomentuj to.
BartoszAdamczewski 15.07.14
1
Nie, ThreeInt32Wrappers kończy się na 12 bajtów, FourInt32Wrappers na 16, FiveInt32Wrappers na 20. Nie widzę nic logicznego w dodaniu pola typu referencyjnego zmieniającego układ tak drastycznie. Zauważ, że całkiem przyjemnie jest zignorować wyrównanie 8-bajtowe, gdy pola są typu Int32. Szczerze mówiąc, nie przejmuję się zbytnio tym, co robi na stosie - ale nie sprawdziłem tego.
Jon Skeet,
9

Podsumowanie patrz odpowiedź @Hans Passant prawdopodobnie powyżej. Layout Sequential nie działa


Niektóre testy:

Jest to zdecydowanie tylko na 64-bitowej wersji, a odniesienie do obiektu „zatruwa” strukturę. 32-bitowy robi to, czego oczekujesz:

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

Gdy tylko odniesienie do obiektu zostanie dodane, wszystkie struktury rozszerzają się do rozmiaru 8 bajtów, a nie 4 bajtów. Rozszerzenie testów:

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

Jak widać, gdy tylko odniesienie zostanie dodane, każde Int32Wrapper stanie się 8 bajtami, więc nie jest to proste wyrównanie. Zmniejszyłem alokację tablicy, ponieważ była to alokacja LoH, która jest inaczej wyrównana.

Ben Adams
źródło
4

Żeby dodać trochę danych do miksu - stworzyłem jeszcze jeden typ z tych, które masz:

struct RefAndTwoInt32Wrappers2
{
    string text;
    TwoInt32Wrappers z;
}

Program pisze:

RefAndTwoInt32Wrappers2: 16

Wygląda więc na to, że TwoInt32Wrappersstruktura jest odpowiednio wyrównana w nowej RefAndTwoInt32Wrappers2strukturze.

Jesse C. Slicer
źródło
Czy używasz wersji 64-bitowej? Wyrównanie jest w porządku w wersji 32-bitowej
Ben Adams,
Moje odkrycia są takie same, jak wszyscy inni dla różnych środowisk.
Jesse C. Slicer,