tipos primitivos ejemplos datos c# .net struct clr memory-alignment

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 campos TwoInt32Wrappers ( 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 de string 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 campos Int32Wrapper 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.