c# - primitivos - ¿Por qué la alineación de estructuras depende de si un tipo de campo es primitivo o definido por el usuario?
tipos de datos primitivos en java ejemplos (4)
En Noda Time v2, nos estamos moviendo a una resolución de nanosegundos. Eso significa que ya no podemos utilizar un número entero de 8 bytes para representar todo el intervalo de tiempo que nos interesa. Eso me ha llevado a investigar el uso de la memoria de las (muchas) estructuras de Noda Time, que a su vez me ha llevado para descubrir una pequeña 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 lo requiera si es posible.
Mi escenario principal es que tengo una struct
que contiene un campo de tipo referencia y otros dos campos de tipo valor, donde esos campos son envoltorios simples para int
. Esperaba que eso se representara como 16 bytes en el CLR de 64 bits (8 para la referencia y 4 para cada uno de los otros), pero por alguna razón usa 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 la 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
Asi que:
- Si no tiene un campo de tipo de referencia, el CLR se complace en
Int32Wrapper
camposTwoInt32Wrappers
(TwoInt32Wrappers
tiene un tamaño de 8) - Incluso con un campo de tipo de referencia, el CLR todavía está contento de empacar los campos
int
conjunto (RefAndTwoInt32s
tiene un tamaño de 16) - Combinando los dos, cada campo
Int32Wrapper
parece estar acolchado / alineado a 8 bytes. (RefAndTwoInt32Wrappers
tiene un tamaño de 24.) - Ejecutar el mismo código en el depurador (pero todavía 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
object
lugar destring
no ayuda (supongo que es "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 continúo agregando campos (en pares para mayor simplicidad), los campos
int
todavía cuentan para 4 bytes, y los camposInt32Wrapper
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 la documentación de referencia) o una sugerencia de cómo puedo obtener una pista para el CLR que me gustaría que los campos se empaquete sin especificar un desplazamiento de campo constante?
Creo que esto es un error. Está viendo el efecto secundario del diseño automático, le gusta alinear campos no triviales con una dirección que es un múltiplo de 8 bytes en el modo de 64 bits. Ocurre incluso cuando aplica explícitamente el atributo [StructLayout(LayoutKind.Sequential)]
. Eso no se supone que suceda
Puedes verlo haciendo que los miembros de struct sean públicos y anexando un código de prueba como este:
var test = new RefAndTwoInt32Wrappers();
test.text = "adsf";
test.x.x = 0x11111111;
test.y.x = 0x22222222;
Console.ReadLine(); // <=== Breakpoint here
Cuando llegue el punto de interrupción, use Debug + Windows + Memory + Memory 1. Cambie a enteros de 4 bytes y ponga &test
en el campo Address:
0x000000E928B5DE98 0ed750e0 000000e9 11111111 00000000 22222222 00000000
0xe90ed750e0
es el puntero de cadena en mi máquina (no el tuyo). Puede ver fácilmente Int32Wrappers
, con los 4 bytes adicionales de relleno que convirtieron el tamaño en 24 bytes. Vuelve a la estructura y pon la cuerda al final. Repite y verás que el puntero de cadena sigue siendo el primero. Violando LayoutKind.Sequential
, tienes LayoutKind.Auto
.
Va a ser difícil convencer a Microsoft para arreglar esto, ha funcionado de esta manera durante demasiado tiempo por lo que cualquier cambio va a estar rompiendo algo . El CLR solo intenta honrar a [StructLayout]
por la versión administrada de una estructura y hacer que sea blittable; en general, se da por vencido rápidamente. Notoriamente para cualquier estructura que contenga un DateTime. Solo obtiene la verdadera garantía de LayoutKind al ordenar una estructura. La versión marshaled ciertamente tiene 16 bytes, como Marshal.SizeOf()
te dirá.
Usar LayoutKind.Explicit
corrige, no lo que quería escuchar.
Resumen vea la respuesta de @Hans Passant probablemente arriba. Layout Sequential no funciona
Algunas pruebas:
Definitivamente solo está en 64 bits y la referencia de objeto "envenena" la estructura. 32 bit hace lo que estás esperando:
Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (32 bit)
ConsoleApplication1.Int32Wrapper: 4
ConsoleApplication1.TwoInt32s: 8
ConsoleApplication1.TwoInt32Wrappers: 8
ConsoleApplication1.ThreeInt32Wrappers: 12
ConsoleApplication1.Ref: 4
ConsoleApplication1.RefAndTwoInt32s: 12
ConsoleApplication1.RefAndTwoInt32Wrappers: 12
ConsoleApplication1.RefAndThreeInt32s: 16
ConsoleApplication1.RefAndThreeInt32Wrappers: 16
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:
Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (64 bit)
ConsoleApplication1.Int32Wrapper: 4
ConsoleApplication1.TwoInt32s: 8
ConsoleApplication1.TwoInt32Wrappers: 8
ConsoleApplication1.ThreeInt32Wrappers: 12
ConsoleApplication1.Ref: 8
ConsoleApplication1.RefAndTwoInt32s: 16
ConsoleApplication1.RefAndTwoInt32sSequential: 16
ConsoleApplication1.RefAndTwoInt32Wrappers: 24
ConsoleApplication1.RefAndThreeInt32s: 24
ConsoleApplication1.RefAndThreeInt32Wrappers: 32
ConsoleApplication1.RefAndFourInt32s: 24
ConsoleApplication1.RefAndFourInt32Wrappers: 40
Como puede ver tan pronto como se agrega la referencia, cada Int32Wrapper se convierte en 8 bytes, por lo que no es una alineación simple. Reduje la asignación de la matriz en caso de que fuera la asignación LoH, que está alineada de forma diferente.
Solo para agregar algunos datos a la mezcla, creé uno más de los que tenía:
struct RefAndTwoInt32Wrappers2
{
string text;
TwoInt32Wrappers z;
}
El programa escribe:
RefAndTwoInt32Wrappers2: 16
Así que parece que la estructura TwoInt32Wrappers
alinea correctamente en la nueva estructura RefAndTwoInt32Wrappers2
.
EDIT2
struct RefAndTwoInt32Wrappers
{
public int x;
public string s;
}
Este código estará alineado en 8 bytes para que la estructura tenga 16 bytes. En comparación esto:
struct RefAndTwoInt32Wrappers
{
public int x,y;
public string s;
}
Se alineará a 4 bytes por lo que esta estructura también tendrá 16 bytes. Entonces la razón fundamental es que la alineación de estructuras en CLR está determinada por la cantidad de campos 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:
struct RefAndTwoInt32Wrappers
{
public int x,y;
public Int32Wrapper z;
public string s;
}
Tendrá 24 bytes {x, y} tendrá 4 bytes cada uno y {z, s} tendrá 8 bytes. Una vez que introduzcamos un tipo de ref en la estructura, CLR siempre alineará nuestra estructura personalizada para que coincida con la alineación de clase.
struct RefAndTwoInt32Wrappers
{
public Int32Wrapper z;
public long l;
public int x,y;
}
Este código tendrá 24 bytes ya que Int32Wrapper se alineará igual que long. Por lo tanto, el contenedor de estructuras personalizado siempre se alineará con el campo alineado más alto / mejor de la estructura o con sus propios campos internos más significativos. Entonces, en el caso de una cadena de ref que está alineada con 8 bytes, la envoltura de la estructura se alineará con eso.
El campo de estructura personalizada final dentro de struct siempre se alineará con el campo de instancia alineado más alto en la estructura. Ahora bien, 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 estructuras tienen tamaños más pequeños (los tamaños exactos de sus campos). Análisis adicional sugiere que esto podría ser un error en el código CLR, pero necesita ser respaldado por evidencia.
Voy a inspeccionar el código cli y publicar más actualizaciones si se encuentra algo útil.
Esta es una estrategia de alineación utilizada por .NET mem allocator.
public static RefAndTwoInt32s[] test = new RefAndTwoInt32s[1];
static void Main()
{
test[0].text = "a";
test[0].x = 1;
test[0].x = 1;
Console.ReadKey();
}
Este código compilado con .net40 bajo x64, en WinDbg permite hacer lo siguiente:
Vamos a encontrar el tipo en el Heap primero:
0:004> !dumpheap -type Ref
Address MT Size
0000000003e72c78 000007fe61e8fb58 56
0000000003e72d08 000007fe039d3b78 40
Statistics:
MT Count TotalSize Class Name
000007fe039d3b78 1 40 RefAndTwoInt32s[]
000007fe61e8fb58 1 56 System.Reflection.RuntimeAssembly
Total 2 objects
Una vez que lo tenemos, veamos qué hay debajo de esa dirección:
0:004> !do 0000000003e72d08
Name: RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass: 000007fe039d3ad0
Size: 40(0x28) bytes
Array: Rank 1, Number of elements 1, Type VALUETYPE
Fields:
None
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:
0:004> !dumparray -details 0000000003e72d08
Name: RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass: 000007fe039d3ad0
Size: 40(0x28) bytes
Array: Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3a58
[0] 0000000003e72d18
Name: RefAndTwoInt32s
MethodTable: 000007fe039d3a58
EEClass: 000007fe03ae2338
Size: 32(0x20) bytes
File: C:/ConsoleApplication8/bin/Release/ConsoleApplication8.exe
Fields:
MT Field Offset Type VT Attr Value Name
000007fe61e8c358 4000006 0 System.String 0 instance 0000000003e72d30 text
000007fe61e8f108 4000007 8 System.Int32 1 instance 1 x
000007fe61e8f108 4000008 c System.Int32 1 instance 0 y
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 cadena ref a: 0000000003e72d18 + 8 bytes EE / padding, terminará en 0000000003e72d30 y este es el punto de mira para la referencia de cadena, y dado que todas las referencias tienen 8 bytes rellenos de su primer campo de datos real esto compensa nuestros 32 bytes para esta estructura.
Veamos si la cadena está acolchada de esa manera:
0:004> !do 0000000003e72d30
Name: System.String
MethodTable: 000007fe61e8c358
EEClass: 000007fe617f3720
Size: 28(0x1c) bytes
File: C:/WINDOWS/Microsoft.Net/assembly/GAC_64/mscorlib/v4.0_4.0.0.0__b77a5c561934e089/mscorlib.dll
String: a
Fields:
MT Field Offset Type VT Attr Value Name
000007fe61e8f108 40000aa 8 System.Int32 1 instance 1 m_stringLength
000007fe61e8d640 40000ab c System.Char 1 instance 61 m_firstChar
000007fe61e8c358 40000ac 18 System.String 0 shared static Empty
>> Domain:Value 0000000001577e90:NotInit <<
Ahora analicemos el programa anterior de la misma manera:
public static RefAndTwoInt32Wrappers[] test = new RefAndTwoInt32Wrappers[1];
static void Main()
{
test[0].text = "a";
test[0].x.x = 1;
test[0].y.x = 1;
Console.ReadKey();
}
0:004> !dumpheap -type Ref
Address MT Size
0000000003c22c78 000007fe61e8fb58 56
0000000003c22d08 000007fe039d3c00 48
Statistics:
MT Count TotalSize Class Name
000007fe039d3c00 1 48 RefAndTwoInt32Wrappers[]
000007fe61e8fb58 1 56 System.Reflection.RuntimeAssembly
Total 2 objects
Nuestra estructura es de 48 bytes ahora.
0:004> !dumparray -details 0000000003c22d08
Name: RefAndTwoInt32Wrappers[]
MethodTable: 000007fe039d3c00
EEClass: 000007fe039d3b58
Size: 48(0x30) bytes
Array: Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3ae0
[0] 0000000003c22d18
Name: RefAndTwoInt32Wrappers
MethodTable: 000007fe039d3ae0
EEClass: 000007fe03ae2338
Size: 40(0x28) bytes
File: C:/ConsoleApplication8/bin/Release/ConsoleApplication8.exe
Fields:
MT Field Offset Type VT Attr Value Name
000007fe61e8c358 4000009 0 System.String 0 instance 0000000003c22d38 text
000007fe039d3a20 400000a 8 Int32Wrapper 1 instance 0000000003c22d20 x
000007fe039d3a20 400000b 10 Int32Wrapper 1 instance 0000000003c22d28 y
Aquí la situación es la misma, si agregamos a 0000000003c22d18 + 8 bytes de cadena ref, terminaremos al inicio del primer contenedor interno donde el valor realmente apunta a la dirección en la que estamos.
Ahora podemos ver que cada valor es una referencia de objeto otra vez. Confirmemos eso mirando a 0000000003c22d20.
0:004> !do 0000000003c22d20
<Note: this object has an invalid CLASS field>
Invalid object
En realidad, eso es correcto ya que es una estructura, la dirección no nos dice nada si esto es un obj o vt.
0:004> !dumpvc 000007fe039d3a20 0000000003c22d20
Name: Int32Wrapper
MethodTable: 000007fe039d3a20
EEClass: 000007fe03ae23c8
Size: 24(0x18) bytes
File: C:/ConsoleApplication8/bin/Release/ConsoleApplication8.exe
Fields:
MT Field Offset Type VT Attr Value Name
000007fe61e8f108 4000001 0 System.Int32 1 instance 1 x
Así que en realidad esto es más parecido a un tipo de Unión que tendrá alineado 8 bytes esta vez (todos los rellenos estarán alineados con la estructura padre). 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 realmente 40 bytes de tamaño.
Por lo tanto, si desea ser más conservador con la memoria, nunca debe empaquetarlo en un tipo de estructura struct struct sino que debe usar matrices simples. Otra forma es asignar memoria fuera de Heap (VirtualAllocEx por ej.) De esta manera se le otorga su propio bloque de memoria y lo administra de la manera que desee.
La última pregunta aquí es por qué de repente podríamos obtener un diseño así. Bueno, si se compara el código jited y el rendimiento de un incremento int [] con struct [] con un incremento de campo contador, el segundo generará una dirección alineada de 8 bytes siendo una unión, pero cuando jited esto se traduce en un código ensamblado más optimizado (singe LEA vs MOV múltiple). Sin embargo, en el caso descrito aquí, el rendimiento será peor, así que mi opinión es que esto es coherente con la implementación de 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 una valor (ya que sería imposible) y hacer relleno estructural allí, lo que resulta en un tamaño de bytes más grande.