Warum hängt die Strukturausrichtung davon ab, ob ein Feldtyp primitiv oder benutzerdefiniert ist?

121

In Noda Time v2 wechseln wir zur Auflösung von Nanosekunden. Das bedeutet, dass wir keine 8-Byte-Ganzzahl mehr verwenden können, um den gesamten Zeitbereich darzustellen, an dem wir interessiert sind. Dies hat mich veranlasst, die Speichernutzung der (vielen) Strukturen von Noda Time zu untersuchen, was mich wiederum geführt hat eine leichte Kuriosität in der Ausrichtungsentscheidung der CLR aufzudecken.

Zum einen wird mir klar , dass dies ist eine Implementierung Entscheidung, und dass das Standardverhalten jederzeit ändern könnte. Ich weiß , dass ich kann ändern Sie es verwenden [StructLayout]und [FieldOffset], aber ich würde lieber mit einer Lösung kommen , die nicht , dass , wenn möglich , erforderte.

Mein Kernszenario ist, dass ich ein structFeld habe, das ein Referenztypfeld und zwei andere Werttypfelder enthält, für die diese Felder einfache Wrapper sind int. Ich hatte gehofft , dass dies in der 64-Bit-CLR als 16 Bytes dargestellt wird (8 für die Referenz und 4 für die anderen), aber aus irgendeinem Grund werden 24 Bytes verwendet. Ich messe den Raum übrigens mit Arrays - ich verstehe, dass das Layout in verschiedenen Situationen unterschiedlich sein kann, aber dies schien ein vernünftiger Ausgangspunkt zu sein.

Hier ist ein Beispielprogramm, das das Problem demonstriert:

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

Und die Kompilierung und Ausgabe auf meinem Laptop:

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

So:

  • Wenn Sie kein Referenztypfeld haben, packt die CLR gerne Int32Wrapper Felder zusammen ( TwoInt32Wrappershat eine Größe von 8).
  • Selbst mit einem Referenztypfeld packt die CLR gerne ein int Felder zusammen ( RefAndTwoInt32shat eine Größe von 16).
  • Kombinieren Sie die beiden jeweils Int32Wrapper scheint Feld auf 8 Bytes aufgefüllt / ausgerichtet zu sein. ( RefAndTwoInt32Wrappershat eine Größe von 24.)
  • Wenn Sie denselben Code im Debugger ausführen (aber immer noch ein Release-Build), wird eine Größe von 12 angezeigt.

Einige andere Experimente haben zu ähnlichen Ergebnissen geführt:

  • Das Einfügen des Referenztypfelds nach den Werttypfeldern hilft nicht
  • Verwenden objectstattstring hilft nicht (ich gehe davon aus, dass es sich um einen beliebigen Referenztyp handelt)
  • Die Verwendung einer anderen Struktur als "Wrapper" um die Referenz hilft nicht
  • Die Verwendung einer generischen Struktur als Wrapper um die Referenz hilft nicht
  • Wenn ich weiterhin Felder hinzufüge (der Einfachheit halber paarweise), zählen intFelder immer noch für 4 Bytes undInt32Wrapper Felder für 8 Bytes
  • Das Hinzufügen [StructLayout(LayoutKind.Sequential, Pack = 4)]zu jeder sichtbaren Struktur ändert nichts an den Ergebnissen

Hat jemand eine Erklärung dafür (idealerweise mit Referenzdokumentation) oder einen Vorschlag, wie ich der CLR einen Hinweis geben kann, dass die Felder gepackt werden sollen, ohne einen konstanten Feldversatz anzugeben?

Jon Skeet
quelle
1
Sie scheinen nicht wirklich zu verwenden, Ref<T>sondern verwenden stringstattdessen, nicht dass es einen Unterschied machen sollte.
Tvanfosson
2
Was passiert, wenn Sie zwei setzen, um eine Struktur mit zwei TwoInt32Wrappersoder einem Int64und einem zu erstellen TwoInt32Wrappers? Wie wäre es, wenn Sie ein Generikum Pair<T1,T2> {public T1 f1; public T2 f2;}erstellen Pair<string,Pair<int,int>>und dann und erstellen Pair<string,Pair<Int32Wrapper,Int32Wrapper>>? Welche Kombinationen zwingen den JITter, Dinge aufzufüllen?
Supercat
7
@supercat: Es ist wahrscheinlich am besten, wenn Sie den Code kopieren und selbst experimentieren - aber Pair<string, TwoInt32Wrappers> es gibt nur 16 Bytes, damit das Problem behoben wird . Faszinierend.
Jon Skeet
9
@SLaks: Manchmal, wenn eine Struktur an nativen Code übergeben wird, kopiert die Runtime alle Daten in eine Struktur mit einem anderen Layout. Marshal.SizeOfgibt die Größe der Struktur zurück, die an nativen Code übergeben werden würde, der keine Beziehung zur Größe der Struktur in .NET-Code haben muss.
Supercat
5
Die interessante Beobachtung: Mono liefert korrekte Ergebnisse. Umgebung: CLR 4.0.30319.17020 unter Unix 3.13.0.24 (64 Bit) Int32Wrapper: 4 TwoInt32s: 8 TwoInt32Wrapper: 8 RefAndTwoInt32s: 16 RefAndTwoInt32Wrappers: 16
AndreyAkinshin

Antworten:

85

Ich denke, das ist ein Fehler. Sie sehen den Nebeneffekt des automatischen Layouts, es mag es, nicht triviale Felder an einer Adresse auszurichten, die im 64-Bit-Modus ein Vielfaches von 8 Bytes ist. Es tritt auch dann auf, wenn Sie das explizit anwenden[StructLayout(LayoutKind.Sequential)] Attribut . Das soll nicht passieren.

Sie können es sehen, indem Sie die Strukturmitglieder öffentlich machen und Testcode wie folgt anhängen:

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

Wenn der Haltepunkt erreicht ist, verwenden Sie Debug + Windows + Speicher + Speicher 1. Wechseln Sie zu 4-Byte-Ganzzahlen und geben Sie &testdas Feld Adresse ein:

 0x000000E928B5DE98  0ed750e0 000000e9 11111111 00000000 22222222 00000000 

0xe90ed750e0ist der Zeichenfolgenzeiger auf meinem Computer (nicht auf Ihrem). Sie können das leicht sehen Int32Wrappers, mit den zusätzlichen 4 Bytes Auffüllen, die die Größe in 24 Bytes verwandelten. Gehen Sie zurück zur Struktur und setzen Sie den String zuletzt. Wiederholen Sie diesen Vorgang, und Sie werden sehen, dass der Zeichenfolgenzeiger immer noch an erster Stelle steht. Du LayoutKind.Sequentialhast es verletzt LayoutKind.Auto.

Es wird schwierig sein, Microsoft davon zu überzeugen, dies zu beheben. Es hat zu lange auf diese Weise funktioniert, sodass jede Änderung etwas kaputt machen wird . Die CLR macht nur einen Versuch, [StructLayout]die verwaltete Version einer Struktur zu ehren und sie blittable zu machen, sie gibt im Allgemeinen schnell auf. Bekannt für jede Struktur, die eine DateTime enthält. Sie erhalten nur beim Marshalling einer Struktur die echte LayoutKind-Garantie. Die gemarshallte Version ist sicherlich 16 Bytes, wie Marshal.SizeOf()Sie sehen werden.

Verwenden Sie LayoutKind.ExplicitFixes, nicht das, was Sie hören wollten.

Hans Passant
quelle
7
"Es wird schwierig sein, Microsoft davon zu überzeugen, dies zu beheben. Es hat zu lange so funktioniert, sodass jede Änderung etwas kaputt macht." Die Tatsache, dass sich dies anscheinend nicht in 32-Bit oder Mono manifestiert, kann helfen (wie in anderen Kommentaren angegeben).
NPSF3000
Die Dokumentation von StructLayoutAttribute ist ziemlich interessant. Grundsätzlich werden nur blittbare Typen über StructLayout im verwalteten Speicher gesteuert. Interessant, wusste das nie.
Michael Stum
@Soner nein es behebt es nicht. Haben Sie das Layout auf beide Felder gesetzt, um 8 zu versetzen? Wenn ja, dann sind x und y gleich und das Ändern eines ändert das andere. Ganz klar nicht, wonach Jon sucht.
BartoszAdamczewski
Das Ersetzen stringdurch einen anderen neuen Referenztyp ( class), auf den man angewendet hat, [StructLayout(LayoutKind.Sequential)]scheint nichts zu ändern. In der entgegengesetzten Richtung ändert sich die Speichernutzung in [StructLayout(LayoutKind.Auto)]den struct Int32WrapperÄnderungen TwoInt32Wrappers.
Jeppe Stig Nielsen
1
"Es wird schwierig sein, Microsoft davon zu überzeugen, dies zu beheben. Es hat zu lange so funktioniert, sodass jede Änderung etwas kaputt macht." xkcd.com/1172
iCodeSometime
19

EDIT2

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

Dieser Code ist 8 Byte ausgerichtet, sodass die Struktur 16 Byte hat. Zum Vergleich:

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

Wird 4 Byte ausgerichtet sein, so dass diese Struktur auch 16 Byte hat. Das Grundprinzip hier ist also, dass die Strukturausrichtung in CLR durch die Anzahl der am meisten ausgerichteten Felder bestimmt wird. Klassen können dies offensichtlich nicht, so dass sie 8 Byte ausgerichtet bleiben.

Wenn wir nun all das kombinieren und eine Struktur erstellen:

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

Es wird 24 Bytes haben {x, y} wird jeweils 4 Bytes haben und {z, s} wird 8 Bytes haben. Sobald wir einen Ref-Typ in die Struktur einführen, richtet CLR unsere benutzerdefinierte Struktur immer so aus, dass sie mit der Klassenausrichtung übereinstimmt.

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

Dieser Code hat 24 Bytes, da Int32Wrapper genauso lange ausgerichtet wird. Der benutzerdefinierte Struktur-Wrapper wird also immer am höchsten / am besten ausgerichteten Feld in der Struktur oder an seinen eigenen internen, höchstwertigen Feldern ausgerichtet. Im Fall einer 8-Byte-ausgerichteten Referenzzeichenfolge wird der Struktur-Wrapper darauf ausgerichtet.

Das abschließende benutzerdefinierte Strukturfeld innerhalb der Struktur wird immer an dem am höchsten ausgerichteten Instanzfeld in der Struktur ausgerichtet. Wenn ich nicht sicher bin, ob dies ein Fehler ist, aber ohne Beweise, werde ich an meiner Meinung festhalten, dass dies eine bewusste Entscheidung sein könnte.


BEARBEITEN

Die Größen sind tatsächlich nur dann genau, wenn sie auf einem Heap zugeordnet sind, aber die Strukturen selbst haben kleinere Größen (die genauen Größen der Felder). Weitere Analysennaht deutet darauf hin, dass dies möglicherweise ein Fehler im CLR-Code ist, der jedoch durch Beweise gestützt werden muss.

Ich werde den CLI-Code überprüfen und weitere Updates veröffentlichen, wenn etwas Nützliches gefunden wird.


Dies ist eine Ausrichtungsstrategie, die vom .NET-Mem-Allokator verwendet wird.

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

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

    Console.ReadKey();
}

Dieser Code, der mit .net40 unter x64 kompiliert wurde, kann in WinDbg Folgendes tun:

Finden wir zuerst den Typ auf dem 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

Sobald wir es haben, können wir sehen, was sich unter dieser Adresse befindet:

    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

Wir sehen, dass dies ein ValueType ist und der, den wir erstellt haben. Da dies ein Array ist, müssen wir den ValueType-Def eines einzelnen Elements im Array abrufen:

    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

Die Struktur hat tatsächlich 32 Bytes, da ihre 16 Bytes für das Auffüllen reserviert sind, sodass in Wirklichkeit jede Struktur von Anfang an mindestens 16 Bytes groß ist.

Wenn Sie 16 Bytes aus Ints und eine Zeichenfolge mit dem Verweis 0000000003e72d18 + 8 Bytes EE / padding hinzufügen, erhalten Sie 0000000003e72d30. Dies ist der Ausgangspunkt für die Zeichenfolgenreferenz. Da alle Referenzen aus dem ersten tatsächlichen Datenfeld mit 8 Byte aufgefüllt sind Dies macht unsere 32 Bytes für diese Struktur wieder wett.

Mal sehen, ob der String tatsächlich so aufgefüllt ist:

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

Lassen Sie uns nun das obige Programm auf die gleiche Weise analysieren:

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

Unsere Struktur ist jetzt 48 Bytes.

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

Hier ist die Situation dieselbe. Wenn wir 0000000003c22d18 + 8 Bytes String ref hinzufügen, landen wir am Anfang des ersten Int-Wrappers, wo der Wert tatsächlich auf die Adresse zeigt, an der wir uns befinden.

Jetzt können wir sehen, dass jeder Wert wieder eine Objektreferenz ist. Lassen Sie uns dies durch einen Blick auf 0000000003c22d20 bestätigen.

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

Eigentlich ist das richtig, da es eine Struktur ist, die uns die Adresse nichts sagt, wenn dies ein obj oder vt ist.

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

In Wirklichkeit ähnelt dies eher einem Union-Typ, bei dem diesmal 8 Byte ausgerichtet werden (alle Auffüllungen werden an der übergeordneten Struktur ausgerichtet). Wenn dies nicht der Fall wäre, würden wir am Ende 20 Bytes haben, und das ist nicht optimal, so dass der Mem-Allokator dies niemals zulässt. Wenn Sie erneut rechnen, stellt sich heraus, dass die Struktur tatsächlich 40 Byte groß ist.

Wenn Sie also konservativer mit dem Speicher umgehen möchten, sollten Sie ihn niemals in einen benutzerdefinierten Strukturtyp packen, sondern stattdessen einfache Arrays verwenden. Eine andere Möglichkeit besteht darin, Speicher außerhalb des Heapspeichers zuzuweisen (z. B. VirtualAllocEx). Auf diese Weise erhalten Sie einen eigenen Speicherblock und verwalten ihn nach Ihren Wünschen.

Die letzte Frage hier ist, warum wir plötzlich so ein Layout bekommen könnten. Wenn Sie den Jited-Code und die Leistung einer int [] -Inkrementierung mit struct [] mit einer Zählerfeld-Inkrementierung vergleichen, generiert die zweite eine 8-Byte-ausgerichtete Adresse, die eine Vereinigung darstellt LEA vs multiple MOV). In dem hier beschriebenen Fall ist die Leistung jedoch tatsächlich schlechter, daher gehe ich davon aus, dass dies mit der zugrunde liegenden CLR-Implementierung übereinstimmt, da es sich um einen benutzerdefinierten Typ handelt, der mehrere Felder haben kann, sodass es möglicherweise einfacher / besser ist, die Startadresse anstelle von a zu setzen Wert (da dies unmöglich wäre) und Strukturauffüllung dort durchführen, was zu einer größeren Bytegröße führt.

BartoszAdamczewski
quelle
1
Wenn ich mir das selbst anschaue, ist die Größe RefAndTwoInt32Wrappers nicht 32 Bytes - es sind 24, was der mit meinem Code angegebenen entspricht. Wenn Sie in die Speicheransicht schauen, anstatt sie zu verwenden dumparray, und im Speicher nach einem Array mit (sagen wir) 3 Elementen mit unterscheidbaren Werten suchen, können Sie deutlich erkennen, dass jedes Element aus einer 8-Byte-Zeichenfolgenreferenz und zwei 8-Byte-Ganzzahlen besteht . Ich vermute, dumparraydass die Werte nur als Referenz angezeigt Int32Wrapperwerden, weil sie nicht wissen, wie Werte angezeigt werden sollen. Diese "Referenzen" zeigen auf sich selbst; Sie sind keine getrennten Werte.
Jon Skeet
1
Ich bin mir nicht ganz sicher, woher Sie das "16-Byte-Auffüllen" beziehen, aber ich vermute, dass dies daran liegt, dass Sie sich die Größe des Array-Objekts ansehen, das "16 Byte + Anzahl * Elementgröße" ist. Ein Array mit der Anzahl 2 hat also eine Größe von 72 (16 + 2 * 24), was dumparrayzeigt.
Jon Skeet
@jon hast du deine Struktur dumm gemacht und überprüft, wie viel Platz sie auf dem Heap einnimmt? Normalerweise wird die Arraygröße am Anfang des Arrays beibehalten, dies kann auch überprüft werden.
BartoszAdamczewski
@jon Die angegebene Größe enthält auch den Offset des Strings, der bei 8 beginnt. Ich glaube nicht, dass diese zusätzlichen 8 Bytes vom Array stammen, da sich der größte Teil des Array-Materials vor der ersten Elementadresse befindet, aber ich werde und überprüfen Kommentar dazu.
BartoszAdamczewski
1
Nein, ThreeInt32Wrappers werden zu 12 Bytes, FourInt32Wrappers zu 16, FiveInt32Wrappers zu 20. Ich sehe nichts Logisches daran, dass ein Referenztypfeld das Layout so drastisch ändert. Beachten Sie auch, dass die 8-Byte-Ausrichtung gerne ignoriert wird, wenn die Felder vom Typ sind Int32. Ich bin nicht sonderlich besorgt darüber, was es auf dem Stapel macht, um ehrlich zu sein - aber ich habe es nicht überprüft.
Jon Skeet
9

Zusammenfassung siehe @ Hans Passants Antwort wahrscheinlich oben. Layout Sequential funktioniert nicht


Einige Tests:

Es ist definitiv nur auf 64bit und die Objektreferenz "vergiftet" die Struktur. 32 Bit macht was Sie erwarten:

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

Sobald die Objektreferenz hinzugefügt wird, werden alle Strukturen auf 8 Byte erweitert, anstatt auf 4 Byte. Erweiterung der Tests:

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

Wie Sie sehen können, wird jeder Int32Wrapper nach dem Hinzufügen der Referenz zu 8 Bytes, was keine einfache Ausrichtung ist. Ich habe die Array-Zuordnung verkleinert, falls es sich um eine LoH-Zuordnung handelt, die unterschiedlich ausgerichtet ist.

Ben Adams
quelle
4

Nur um dem Mix einige Daten hinzuzufügen - ich habe einen weiteren Typ aus den von Ihnen vorhandenen erstellt:

struct RefAndTwoInt32Wrappers2
{
    string text;
    TwoInt32Wrappers z;
}

Das Programm schreibt aus:

RefAndTwoInt32Wrappers2: 16

Es sieht also so aus, als würde die TwoInt32WrappersStruktur in der neuen RefAndTwoInt32Wrappers2Struktur richtig ausgerichtet .

Jesse C. Slicer
quelle
Laufen Sie 64 Bit? Die Ausrichtung ist in 32 Bit in Ordnung
Ben Adams
Meine Ergebnisse sind die gleichen wie alle anderen für die verschiedenen Umgebungen.
Jesse C. Slicer