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?
TwoInt32Wrappers
oder einem Int64
und 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?
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.
Ref<T>
sondern verwendenstring
stattdessen, nicht dass es einen Unterschied machen sollte.