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?
Ref<T>
sino que lo está utilizandostring
, no es que deba marcar la diferencia.TwoInt32Wrappers
, o anInt64
y aTwoInt32Wrappers
? ¿Qué tal si creas un genéricoPair<T1,T2> {public T1 f1; public T2 f2;}
y luego creasPair<string,Pair<int,int>>
yPair<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.Respuestas:
Creo que esto es un error. Está viendo el efecto secundario del diseño automático, le gusta alinear campos no triviales a una dirección que es un múltiplo de 8 bytes en modo de 64 bits. Ocurre incluso cuando aplica explícitamente el
[StructLayout(LayoutKind.Sequential)]
atributo. Eso no se supone que suceda.Puede verlo haciendo públicos los miembros de la estructura y agregando un código de prueba como este:
Cuando llegue el punto de interrupción, use Debug + Windows + Memory + Memory 1. Cambie a enteros de 4 bytes y coloque
&test
en el campo Dirección:0xe90ed750e0
es el puntero de cadena en mi máquina (no la tuya). Puede ver fácilmente elInt32Wrappers
, con los 4 bytes adicionales de relleno que convirtió el tamaño en 24 bytes. Regrese a la estructura y coloque la cadena al final. Repita y verá que el puntero de cadena sigue siendo el primero. ViolandoLayoutKind.Sequential
, tienesLayoutKind.Auto
.Será difícil convencer a Microsoft para que solucione esto, ha funcionado de esta manera durante demasiado tiempo, por lo que cualquier cambio va a romper algo . El CLR solo intenta honrar
[StructLayout]
la versión administrada de una estructura y hacer que se pueda borrar, en general se rinde rápidamente. Notoriamente para cualquier estructura que contenga un DateTime. Solo obtienes la verdadera garantía LayoutKind al organizar una estructura. La versión ordenada ciertamente es de 16 bytes, comoMarshal.SizeOf()
te diré.El uso lo
LayoutKind.Explicit
arregla, no lo que querías escuchar.fuente
string
por otro nuevo tipo de referencia (class
) al que se ha aplicado[StructLayout(LayoutKind.Sequential)]
no parece cambiar nada. En la dirección opuesta, aplicando[StructLayout(LayoutKind.Auto)]
a losstruct Int32Wrapper
cambios el uso de memoria enTwoInt32Wrappers
.EDIT2
Este código estará alineado en 8 bytes, por lo que la estructura tendrá 16 bytes. En comparación esto:
Estará alineado en 4 bytes, por lo que esta estructura también tendrá 16 bytes. Entonces, la razón aquí es que la alineación de estructura en CLR está determinada por el número de campos más alineados, las clases obviamente no pueden hacer eso, por lo que permanecerán alineados en 8 bytes.
Ahora si combinamos todo eso y creamos struct:
Tendrá 24 bytes {x, y} tendrá 4 bytes cada uno y {z, s} tendrá 8 bytes. Una vez que introduzcamos un tipo de referencia en la estructura, CLR siempre alineará nuestra estructura personalizada para que coincida con la alineación de la clase.
Este código tendrá 24 bytes ya que Int32Wrapper estará alineado igual que siempre. Por lo tanto, el contenedor de estructura personalizado siempre se alineará con el campo más alto / mejor alineado de la estructura o con sus propios campos internos más significativos. Entonces, en el caso de una cadena de referencia que esté alineada con 8 bytes, el contenedor de estructura se alineará con eso.
El campo de estructura personalizado final dentro de la estructura siempre se alineará con el campo de instancia alineado más alto en la estructura. Ahora, si no estoy seguro de si esto es un error, pero sin alguna evidencia, voy a mantener mi opinión de que esta podría ser una decisión consciente.
EDITAR
Los tamaños son realmente precisos solo cuando se asignan en un montón, pero las propias estructuras tienen tamaños más pequeños (los tamaños exactos de sus campos). Un análisis más detallado sugiere que esto podría ser un error en el código CLR, pero debe respaldarse con evidencia.
Inspeccionaré el código cli y publicaré más actualizaciones si se encuentra algo útil.
Esta es una estrategia de alineación utilizada por el asignador de mem .NET.
Este código compilado con .net40 bajo x64, en WinDbg hagamos lo siguiente:
Busquemos primero el tipo en el montón:
Una vez que lo tengamos, veamos qué hay debajo de esa dirección:
Vemos que este es un ValueType y es el que creamos. Como se trata de una matriz, necesitamos obtener la definición de ValueType de un único elemento en la matriz:
La estructura es en realidad de 32 bytes ya que sus 16 bytes están reservados para el relleno, por lo que en realidad cada estructura tiene al menos 16 bytes de tamaño desde el principio.
si agrega 16 bytes de ints y una referencia de cadena a: 0000000003e72d18 + 8 bytes EE / relleno, terminará en 0000000003e72d30 y este es el punto de partida para la referencia de cadena, y dado que todas las referencias están rellenadas con 8 bytes desde su primer campo de datos real Esto compensa nuestros 32 bytes para esta estructura.
Veamos si la cadena está realmente acolchada de esa manera:
Ahora analicemos el programa anterior de la misma manera:
Nuestra estructura es de 48 bytes ahora.
Aquí la situación es la misma, si agregamos a 0000000003c22d18 + 8 bytes de cadena de referencia terminaremos al comienzo del primer contenedor Int donde el valor realmente apunta a la dirección en la que estamos.
Ahora podemos ver que cada valor es una referencia de objeto nuevamente, confirmemos al mirar 0000000003c22d20.
En realidad, eso es correcto, ya que es una estructura, la dirección no nos dice nada si se trata de un obj o vt.
Entonces, en realidad, esto es más parecido a un tipo de Unión que obtendrá 8 bytes alineados esta vez (todos los rellenos estarán alineados con la estructura principal). Si no fuera así, terminaríamos con 20 bytes y eso no es óptimo, por lo que el asignador de memoria nunca permitirá que suceda. Si vuelve a hacer los cálculos, resultará que la estructura tiene un tamaño de 40 bytes.
Entonces, si desea ser más conservador con la memoria, nunca debe empacarlo en un tipo de estructura personalizada de estructura, sino que debe usar matrices simples. Otra forma es asignar memoria fuera del montón (VirtualAllocEx, por ejemplo) de esta manera se le da su propio bloque de memoria y lo administra de la manera que desee.
La pregunta final aquí es por qué de repente podríamos obtener un diseño como ese. Bueno, si compara el código editado y el rendimiento de un incremento int [] con struct [] con un incremento de campo de contador, el segundo generará una dirección alineada de 8 bytes que es una unión, pero cuando se edita esto se traduce en un código de ensamblaje más optimizado (singe LEA vs MOV múltiple). Sin embargo, en el caso descrito aquí, el rendimiento será realmente peor, así que mi opinión es que esto es consistente con la implementación CLR subyacente, ya que es un tipo personalizado que puede tener múltiples campos, por lo que puede ser más fácil / mejor poner la dirección de inicio en lugar de un valor (ya que sería imposible) y estructurar el relleno allí, lo que resulta en un tamaño de byte más grande.
fuente
RefAndTwoInt32Wrappers
no es de 32 bytes, es 24, que es lo mismo que se informa con mi código. Si mira en la vista de memoria en lugar de usardumparray
, y mira en la memoria una matriz con (digamos) 3 elementos con valores distinguibles, puede ver claramente que cada elemento consta de una referencia de cadena de 8 bytes y dos enteros de 8 bytes . Sospecho quedumparray
muestra los valores como referencias simplemente porque no sabe cómo mostrar losInt32Wrapper
valores. Esas "referencias" apuntan a sí mismas; No son valores separados.dumparray
muestra.Int32
. No estoy demasiado preocupado por lo que hace en la pila, para ser honesto, pero no lo he comprobado.Resumen ver la respuesta de @Hans Passant probablemente arriba. Secuencial de diseño no funciona
Algunas pruebas:
Definitivamente es solo en 64 bits y el objeto de referencia "envenena" la estructura. 32 bit hace lo que esperas:
Tan pronto como se agrega la referencia del objeto, todas las estructuras se expanden para tener 8 bytes en lugar de su tamaño de 4 bytes. Ampliando las pruebas:
Como puede ver tan pronto como se agrega la referencia, cada Int32Wrapper se convierte en 8 bytes, por lo que no es una simple alineación. Reduje la asignación de matriz en caso de que fuera una asignación de LoH que está alineada de manera diferente.
fuente
Solo para agregar algunos datos a la mezcla: creé un tipo más de los que tenía:
El programa escribe:
Por lo tanto, parece que la
TwoInt32Wrappers
estructura se alinea correctamente en la nuevaRefAndTwoInt32Wrappers2
estructura.fuente