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 struct
Feld 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 (TwoInt32Wrappers
hat eine Größe von 8). - Selbst mit einem Referenztypfeld packt die CLR gerne ein
int
Felder zusammen (RefAndTwoInt32s
hat eine Größe von 16). - Kombinieren Sie die beiden jeweils
Int32Wrapper
scheint Feld auf 8 Bytes aufgefüllt / ausgerichtet zu sein. (RefAndTwoInt32Wrappers
hat 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
object
stattstring
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
int
Felder 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?
Ref<T>
sondern verwendenstring
stattdessen, nicht dass es einen Unterschied machen sollte.TwoInt32Wrappers
oder einemInt64
und einem zu erstellenTwoInt32Wrappers
? Wie wäre es, wenn Sie ein GenerikumPair<T1,T2> {public T1 f1; public T2 f2;}
erstellenPair<string,Pair<int,int>>
und dann und erstellenPair<string,Pair<Int32Wrapper,Int32Wrapper>>
? Welche Kombinationen zwingen den JITter, Dinge aufzufüllen?Pair<string, TwoInt32Wrappers>
es gibt nur 16 Bytes, damit das Problem behoben wird . Faszinierend.Marshal.SizeOf
gibt 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.Antworten:
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:
Wenn der Haltepunkt erreicht ist, verwenden Sie Debug + Windows + Speicher + Speicher 1. Wechseln Sie zu 4-Byte-Ganzzahlen und geben Sie
&test
das Feld Adresse ein:0xe90ed750e0
ist der Zeichenfolgenzeiger auf meinem Computer (nicht auf Ihrem). Sie können das leicht sehenInt32Wrappers
, 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. DuLayoutKind.Sequential
hast es verletztLayoutKind.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, wieMarshal.SizeOf()
Sie sehen werden.Verwenden Sie
LayoutKind.Explicit
Fixes, nicht das, was Sie hören wollten.quelle
string
durch 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)]
denstruct Int32Wrapper
ÄnderungenTwoInt32Wrappers
.EDIT2
Dieser Code ist 8 Byte ausgerichtet, sodass die Struktur 16 Byte hat. Zum Vergleich:
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:
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.
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.
Dieser Code, der mit .net40 unter x64 kompiliert wurde, kann in WinDbg Folgendes tun:
Finden wir zuerst den Typ auf dem Heap:
Sobald wir es haben, können wir sehen, was sich unter dieser Adresse befindet:
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:
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:
Lassen Sie uns nun das obige Programm auf die gleiche Weise analysieren:
Unsere Struktur ist jetzt 48 Bytes.
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.
Eigentlich ist das richtig, da es eine Struktur ist, die uns die Adresse nichts sagt, wenn dies ein obj oder vt ist.
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.
quelle
RefAndTwoInt32Wrappers
nicht 32 Bytes - es sind 24, was der mit meinem Code angegebenen entspricht. Wenn Sie in die Speicheransicht schauen, anstatt sie zu verwendendumparray
, 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,dumparray
dass die Werte nur als Referenz angezeigtInt32Wrapper
werden, weil sie nicht wissen, wie Werte angezeigt werden sollen. Diese "Referenzen" zeigen auf sich selbst; Sie sind keine getrennten Werte.dumparray
zeigt.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.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:
Sobald die Objektreferenz hinzugefügt wird, werden alle Strukturen auf 8 Byte erweitert, anstatt auf 4 Byte. Erweiterung der Tests:
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.
quelle
Nur um dem Mix einige Daten hinzuzufügen - ich habe einen weiteren Typ aus den von Ihnen vorhandenen erstellt:
Das Programm schreibt aus:
Es sieht also so aus, als würde die
TwoInt32Wrappers
Struktur in der neuenRefAndTwoInt32Wrappers2
Struktur richtig ausgerichtet .quelle