En Noda Time v2, nos estamos moviendo a una resolución de nanosegundos. Eso significa que ya no podemos usar un número entero de 8 bytes para representar todo el rango de tiempo que nos interesa. Eso me ha llevado a investigar el uso de memoria de las (muchas) estructuras de Noda Time, lo que a su vez me ha llevado para descubrir una ligera rareza en la decisión de alineación del CLR.
En primer lugar, me doy cuenta de que esta es una decisión de implementación y que el comportamiento predeterminado podría cambiar en cualquier momento. Me doy cuenta de que puedo modificarlo usando [StructLayout]
y [FieldOffset]
, pero prefiero encontrar una solución que no requiera eso si es posible.
Mi escenario principal es que tengo un campo struct
que contiene un campo de tipo de referencia y otros dos campos de tipo de valor, donde esos campos son envoltorios simples int
. Había esperado que eso puede representar como 16 bytes en el CLR de 64 bits (8 de la referencia y 4 para cada uno de los otros), pero por alguna razón se trata de utilizar 24 bytes. Por cierto, estoy midiendo el espacio usando matrices: entiendo que el diseño puede ser diferente en diferentes situaciones, pero esto se sintió como un punto de partida razonable.
Aquí hay un programa de muestra que demuestra el 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);
}
}
Y la compilación y salida en mi computadora portátil:
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
Entonces:
- Si no tiene un campo de tipo de referencia, el CLR se complace en
Int32Wrapper
agrupar los campos (TwoInt32Wrappers
tiene un tamaño de 8) - Incluso con un campo de tipo de referencia, el CLR sigue contento de
int
agrupar campos (RefAndTwoInt32s
tiene un tamaño de 16) - Combinando los dos, cada
Int32Wrapper
campo parece estar relleno / alineado a 8 bytes. (RefAndTwoInt32Wrappers
tiene un tamaño de 24) - Ejecutar el mismo código en el depurador (pero aún una versión de lanzamiento) muestra un tamaño de 12.
Algunos otros experimentos han arrojado resultados similares:
- Poner el campo de tipo de referencia después de los campos de tipo de valor no ayuda
- Usar en
object
lugar destring
no ayuda (espero que sea "cualquier tipo de referencia") - Usar otra estructura como "envoltorio" alrededor de la referencia no ayuda
- Usar una estructura genérica como envoltorio alrededor de la referencia no ayuda
- Si sigo agregando campos (en pares para simplificar), los
int
campos aún cuentan para 4 bytes y losInt32Wrapper
campos cuentan para 8 bytes - Agregar
[StructLayout(LayoutKind.Sequential, Pack = 4)]
a cada estructura a la vista no cambia los resultados
¿Alguien tiene alguna explicación para esto (idealmente con documentación de referencia) o una sugerencia de cómo puedo dar una pista al CLR de que me gustaría que los campos se empaqueten sin especificar un desplazamiento de campo constante?
TwoInt32Wrappers
, o an Int64
y a TwoInt32Wrappers
? ¿Qué tal si creas un genérico Pair<T1,T2> {public T1 f1; public T2 f2;}
y luego creas Pair<string,Pair<int,int>>
y Pair<string,Pair<Int32Wrapper,Int32Wrapper>>
? ¿Qué combinaciones obligan al JITter a rellenar las cosas?
Pair<string, TwoInt32Wrappers>
le da solo 16 bytes, por lo que eso resolvería el problema. Fascinante.
Marshal.SizeOf
devolverá el tamaño de la estructura que se pasaría al código nativo, que no necesita tener ninguna relación con el tamaño de la estructura en el código .NET.
Ref<T>
sino que lo está utilizandostring
, no es que deba marcar la diferencia.