In Noda Time v2, stiamo passando alla risoluzione in nanosecondi. Ciò significa che non possiamo più utilizzare un numero intero a 8 byte per rappresentare l'intero intervallo di tempo a cui siamo interessati. Questo mi ha spinto a indagare sull'utilizzo della memoria delle (molte) strutture di Noda Time, che a sua volta mi ha portato per scoprire una leggera stranezza nella decisione di allineamento del CLR.
In primo luogo, mi rendo conto che questa è una decisione di implementazione e che il comportamento predefinito potrebbe cambiare in qualsiasi momento. Mi rendo conto che posso modificarlo usando [StructLayout]
e [FieldOffset]
, ma preferirei trovare una soluzione che non lo richiedesse, se possibile.
Il mio scenario principale è che ho un struct
che contiene un campo di tipo riferimento e altri due campi di tipo valore, in cui quei campi sono semplici wrapper per int
. Avevo sperato che sarebbe stato rappresentato come 16 byte sul CLR a 64 bit (8 per il riferimento e 4 per ciascuno degli altri), ma per qualche motivo utilizza 24 byte. Sto misurando lo spazio usando gli array, a proposito: capisco che il layout può essere diverso in situazioni diverse, ma questo mi è sembrato un punto di partenza ragionevole.
Ecco un programma di esempio che dimostra il problema:
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);
}
}
E la compilazione e l'output sul mio 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
Così:
- Se non si dispone di un campo del tipo di riferimento, il CLR è felice di
Int32Wrapper
raggruppare i campi insieme (TwoInt32Wrappers
ha una dimensione di 8) - Anche con un campo del tipo di riferimento, il CLR è ancora felice di
int
raggruppare i campi insieme (RefAndTwoInt32s
ha una dimensione di 16) - Combinando i due, ogni
Int32Wrapper
campo sembra essere riempito / allineato a 8 byte. (RefAndTwoInt32Wrappers
ha una dimensione di 24.) - L'esecuzione dello stesso codice nel debugger (ma ancora una build di rilascio) mostra una dimensione di 12.
Alcuni altri esperimenti hanno prodotto risultati simili:
- Mettere il campo del tipo di riferimento dopo i campi del tipo di valore non aiuta
- Usare
object
invece distring
non aiuta (immagino sia "qualsiasi tipo di riferimento") - Usare un'altra struttura come "wrapper" attorno al riferimento non aiuta
- L'uso di una struttura generica come wrapper attorno al riferimento non aiuta
- Se continuo ad aggiungere campi (a coppie per semplicità), i
int
campi contano ancora per 4 byte e iInt32Wrapper
campi contano per 8 byte - L'aggiunta
[StructLayout(LayoutKind.Sequential, Pack = 4)]
a ogni struttura in vista non cambia i risultati
Qualcuno ha qualche spiegazione per questo (idealmente con la documentazione di riferimento) o un suggerimento su come posso ottenere un suggerimento al CLR che vorrei che i campi fossero impacchettati senza specificare un offset di campo costante?
TwoInt32Wrappers
o un Int64
e a TwoInt32Wrappers
? Che ne dici se crei un generico Pair<T1,T2> {public T1 f1; public T2 f2;}
e poi crei Pair<string,Pair<int,int>>
e Pair<string,Pair<Int32Wrapper,Int32Wrapper>>
? Quali combinazioni costringono il JITter a riempire le cose?
Pair<string, TwoInt32Wrappers>
non fare solo 16 byte, in modo che sarebbe affrontare la questione. Affascinante.
Marshal.SizeOf
restituirà la dimensione della struttura che verrebbe passata al codice nativo, che non necessita di alcuna relazione con la dimensione della struttura nel codice .NET.
Ref<T>
ma stai usandostring
invece, non che dovrebbe fare la differenza.