Por encima de una matriz .NET?

Estaba tratando de determinar la sobrecarga del encabezado en una matriz .NET (en un proceso de 32 bits) usando este código:

long bytes1 = GC.GetTotalMemory(false); object[] array = new object[10000]; for (int i = 0; i < 10000; i++) array[i] = new int[1]; long bytes2 = GC.GetTotalMemory(false); array[0] = null; // ensure no garbage collection before this point Console.WriteLine(bytes2 - bytes1); // Calculate array overhead in bytes by subtracting the size of // the array elements (40000 for object[10000] and 4 for each // array), and dividing by the number of arrays (10001) Console.WriteLine("Array overhead: {0:0.000}", ((double)(bytes2 - bytes1) - 40000) / 10001 - 4); Console.Write("Press any key to continue..."); Console.ReadKey(); 

El resultado fue

  204800 Array overhead: 12.478 

En un proceso de 32 bits, el objeto [1] debe tener el mismo tamaño que int [1], pero de hecho la sobrecarga salta en 3.28 bytes para

  237568 Array overhead: 15.755 

Alguien sabe por qué?

(Por cierto, si alguien tiene curiosidad, la sobrecarga para objetos que no son de matriz, por ejemplo, (objeto) i en el ciclo anterior, es de aproximadamente 8 bytes (8.384). Escuché que son 16 bytes en procesos de 64 bits).

Aquí hay un progtwig breve pero completo (IMO) para demostrar lo mismo:

 using System; class Test { const int Size = 100000; static void Main() { object[] array = new object[Size]; long initialMemory = GC.GetTotalMemory(true); for (int i = 0; i < Size; i++) { array[i] = new string[0]; } long finalMemory = GC.GetTotalMemory(true); GC.KeepAlive(array); long total = finalMemory - initialMemory; Console.WriteLine("Size of each element: {0:0.000} bytes", ((double)total) / Size); } } 

Pero obtengo los mismos resultados: la sobrecarga para cualquier matriz de tipo de referencia es de 16 bytes, mientras que la sobrecarga para cualquier matriz de tipo de valor es de 12 bytes. Todavía estoy tratando de averiguar por qué es así, con la ayuda de las especificaciones CLI. No olvide que las matrices de tipo de referencia son covariantes, que pueden ser relevantes ...

EDITAR: con la ayuda de cordbg, puedo confirmar la respuesta de Brian: el puntero de tipo de una matriz de tipo referencia es el mismo independientemente del tipo de elemento real. Presumiblemente hay algo de object.GetType() en object.GetType() (que no es virtual, recuerda) para dar cuenta de esto.

Entonces, con el código de:

 object[] x = new object[1]; string[] y = new string[1]; int[] z = new int[1]; z[0] = 0x12345678; lock(z) {} 

Terminamos con algo como lo siguiente:

 Variables: x=(0x1f228c8)  y=(0x1f228dc)  z=(0x1f228f0)  Memory: 0x1f228c4: 00000000 003284dc 00000001 00326d54 00000000 // Data for x 0x1f228d8: 00000000 003284dc 00000001 00329134 00000000 // Data for y 0x1f228ec: 00000000 00d443fc 00000001 12345678 // Data for z 

Tenga en cuenta que he descargado la memoria 1 palabra antes del valor de la variable en sí.

Para y , los valores son:

  • El bloque de sincronización, utilizado para bloquear el código hash (o un locking delgado - ver el comentario de Brian)
  • Tipo de puntero
  • Tamaño de la matriz
  • Puntero del elemento
  • Referencia nula (primer elemento)

Para z , los valores son:

  • Bloque de sincronización
  • Tipo de puntero
  • Tamaño de la matriz
  • 0x12345678 (primer elemento)

Las matrices de tipos de valores diferentes (byte [], int [] etc.) terminan con diferentes punteros de tipo, mientras que todas las matrices de tipo de referencia utilizan el mismo puntero de tipo, pero tienen un puntero de tipo de elemento diferente. El puntero de tipo de elemento tiene el mismo valor que encontraría como puntero de tipo para un objeto de ese tipo. Entonces, si miramos la memoria de un objeto de cadena en la ejecución anterior, tendría un puntero de tipo 0x00329134.

La palabra antes del puntero tipo ciertamente tiene algo que ver con el monitor o el código hash: al llamar a GetHashCode() rellena ese bit de memoria, y creo que el objeto predeterminado.GetHashCode object.GetHashCode() obtiene un bloque de sincronización para garantizar la singularidad del código hash para la vida del objeto. Sin embargo, solo haciendo lock(x){} no hizo nada, lo que me sorprendió ...

Por cierto, todo esto solo es válido para tipos de "vectores": en el CLR, un tipo de "vector" es un conjunto de dimensiones únicas con un límite inferior de 0. Otras matrices tendrán un diseño diferente, por un lado , necesitarían el límite inferior almacenado ...

Hasta ahora, esto ha sido una experimentación, pero aquí están las conjeturas: la razón por la cual el sistema se está implementando de la manera que lo hizo. A partir de ahora, realmente estoy adivinando.

  • Todas las matrices de object[] pueden compartir el mismo código JIT. Se comportarán de la misma manera en términos de asignación de memoria, acceso a la matriz, propiedad de Length y (de manera importante) el diseño de las referencias para la GC. Compare eso con las matrices de tipo de valor, donde los diferentes tipos de valores pueden tener diferentes "huellas" de GC (por ejemplo, uno podría tener un byte y luego una referencia, otros no tendrán referencias, etc.).
  • Cada vez que asigna un valor dentro de un object[] el tiempo de ejecución debe verificar que sea válido. Necesita verificar que el tipo de objeto cuya referencia está utilizando para el nuevo valor del elemento sea compatible con el tipo de elemento de la matriz. Por ejemplo:

     object[] x = new object[1]; object[] y = new string[1]; x[0] = new object(); // Valid y[0] = new object(); // Invalid - will throw an exception 

Esta es la covarianza que mencioné anteriormente. Ahora que esto va a suceder para cada tarea , tiene sentido reducir el número de indirecciones. En particular, sospecho que realmente no desea soplar el caché al tener que ir al objeto tipo para cada asignación para obtener el tipo de elemento. Sospecho (y mi ensamblaje x86 no es lo suficientemente bueno para verificar esto) que la prueba es algo así como:

  • ¿El valor a copiar es una referencia nula? Si es así, está bien. (Hecho.)
  • Busca el puntero de tipo del objeto al que apuntan los puntos de referencia.
  • ¿Es ese puntero de tipo el mismo que el puntero de tipo de elemento (simple comprobación de igualdad binaria)? Si es así, está bien. (Hecho.)
  • ¿Es compatible con la asignación de puntero de tipo con el puntero de tipo de elemento? (Comprobación mucho más complicada, con herencia e interfaces involucradas.) De ser así, está bien; de lo contrario, arroje una excepción.

Si podemos terminar la búsqueda en los primeros tres pasos, no hay mucha indirección, lo cual es bueno para algo que sucederá tan a menudo como las asignaciones de matriz. Nada de esto tiene que suceder para las asignaciones de tipo de valor, porque eso es estadísticamente verificable.

Entonces, es por eso que creo que las matrices de tipo de referencia son un poco más grandes que las matrices de tipo de valor.

Gran pregunta - realmente interesante para profundizar 🙂

Array es un tipo de referencia. Todos los tipos de referencia llevan dos campos de palabras adicionales. La referencia de tipo y un campo de índice SyncBlock, que entre otras cosas se utiliza para implementar lockings en el CLR. Por lo tanto, la sobrecarga de tipo en los tipos de referencia es de 8 bytes en 32 bits. Además de eso, la matriz también almacena la longitud que es otros 4 bytes. Esto trae la sobrecarga total a 12 bytes.

Y acabo de aprender de la respuesta de Jon Skeet, las matrices de tipos de referencia tienen una sobrecarga adicional de 4 bytes. Esto se puede confirmar con WinDbg. Resulta que la palabra adicional es otra referencia de tipo para el tipo almacenado en la matriz. Todas las matrices de tipos de referencia se almacenan internamente como object[] , con la referencia adicional al tipo de objeto del tipo real. Entonces, una string[] es realmente solo un object[] con una referencia de tipo adicional a la string tipo. Para detalles, ver abajo.

Valores almacenados en matrices: las matrices de tipos de referencia contienen referencias a objetos, por lo que cada entrada en la matriz es del tamaño de una referencia (es decir, 4 bytes en 32 bits). Las matrices de tipos de valores almacenan los valores en línea y, por lo tanto, cada elemento ocupará el tamaño del tipo en cuestión.

Esta pregunta también puede ser de interés: C # List size vs double [] size

Detalles de Gory

Considera el siguiente código

 var strings = new string[1]; var ints = new int[1]; strings[0] = "hello world"; ints[0] = 42; 

Adjuntar WinDbg muestra lo siguiente:

Primero echemos un vistazo a la matriz de tipo de valor.

 0:000> !dumparray -details 017e2acc Name: System.Int32[] MethodTable: 63b9aa40 EEClass: 6395b4d4 Size: 16(0x10) bytes Array: Rank 1, Number of elements 1, Type Int32 Element Methodtable: 63b9aaf0 [0] 017e2ad4 Name: System.Int32 MethodTable 63b9aaf0 EEClass: 6395b548 Size: 12(0xc) bytes (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) Fields: MT Field Offset Type VT Attr Value Name 63b9aaf0 40003f0 0 System.Int32 1 instance 42 m_value <=== Our value 0:000> !objsize 017e2acc sizeof(017e2acc) = 16 ( 0x10) bytes (System.Int32[]) 0:000> dd 017e2acc -0x4 017e2ac8 00000000 63b9aa40 00000001 0000002a <=== That's the value 

Primero volcamos la matriz y el elemento con un valor de 42. Como se puede ver, el tamaño es de 16 bytes. Eso es 4 bytes para el valor int32 mismo, 8 bytes para la sobrecarga del tipo de referencia regular y otros 4 bytes para la longitud de la matriz.

El volcado sin procesar muestra el SyncBlock, la tabla de métodos para int[] , la longitud y el valor de 42 (2a en hexadecimal). Observe que SyncBlock se encuentra justo en frente de la referencia del objeto.

A continuación, miremos la string[] para descubrir para qué se usa la palabra adicional.

 0:000> !dumparray -details 017e2ab8 Name: System.String[] MethodTable: 63b74ed0 EEClass: 6395a8a0 Size: 20(0x14) bytes Array: Rank 1, Number of elements 1, Type CLASS Element Methodtable: 63b988a4 [0] 017e2a90 Name: System.String MethodTable: 63b988a4 EEClass: 6395a498 Size: 40(0x28) bytes <=== Size of the string (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) String: hello world Fields: MT Field Offset Type VT Attr Value Name 63b9aaf0 4000096 4 System.Int32 1 instance 12 m_arrayLength 63b9aaf0 4000097 8 System.Int32 1 instance 11 m_stringLength 63b99584 4000098 c System.Char 1 instance 68 m_firstChar 63b988a4 4000099 10 System.String 0 shared static Empty >> Domain:Value 00226438:017e1198 << 63b994d4 400009a 14 System.Char[] 0 shared static WhitespaceChars >> Domain:Value 00226438:017e1760 << 0:000> !objsize 017e2ab8 sizeof(017e2ab8) = 60 ( 0x3c) bytes (System.Object[]) <=== Notice the underlying type of the string[] 0:000> dd 017e2ab8 -0x4 017e2ab4 00000000 63b74ed0 00000001 63b988a4 <=== Method table for string 017e2ac4 017e2a90 <=== Address of the string in memory 0:000> !dumpmt 63b988a4 EEClass: 6395a498 Module: 63931000 Name: System.String mdToken: 02000024 (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) BaseSize: 0x10 ComponentSize: 0x2 Number of IFaces in IFaceMap: 7 Slots in VTable: 196 

Primero volcamos la matriz y la cadena. A continuación, volcamos el tamaño de la string[] . Observe que WinDbg enumera el tipo como System.Object[] aquí. El tamaño del objeto en este caso incluye la cadena en sí, por lo que el tamaño total es el 20 de la matriz más el 40 de la cadena.

Al eliminar los bytes brutos de la instancia, podemos ver lo siguiente: Primero tenemos SyncBlock, luego seguimos la tabla de métodos para el object[] , luego la longitud de la matriz. Después de eso, encontramos los 4 bytes adicionales con la referencia a la tabla de métodos para cadena. Esto se puede verificar con el comando dumpmt como se muestra arriba. Finalmente, encontramos la referencia única a la instancia de cadena real.

En conclusión

La sobrecarga para las matrices se puede desglosar de la siguiente manera (en 32 bits)

  • 4 bytes SyncBlock
  • 4 bytes para la tabla de métodos (referencia de tipo) para la matriz en sí
  • 4 bytes para la longitud de la matriz
  • Las matrices de tipos de referencia agregan otros 4 bytes para contener la tabla de métodos del tipo de elemento real (las matrices de tipo de referencia son object[] bajo el capó)

Es decir, la sobrecarga es de 12 bytes para las matrices de tipo de valor y de 16 bytes para las matrices de tipo de referencia .

Creo que está haciendo suposiciones erróneas durante la medición, ya que la asignación de memoria (a través de GetTotalMemory) durante su ciclo puede ser diferente de la memoria real requerida solo para las matrices: la memoria puede asignarse en bloques más grandes, puede haber otros objetos en memoria que se recupera durante el ciclo, etc.

Aquí hay algo de información sobre la sobrecarga de la matriz:

  • Matrices indocumentadas
  • Artículo de Jeffrey Richter
  • .Net Tipo Internals

Debido a que la gestión de heap (dado que se ocupa de GetTotalMemory) solo puede asignar bloques bastante grandes, que CLR asigna a los últimos en trozos más pequeños para fines de progtwigción.

Lo siento por el offtopic pero encontré información interesante sobre la memoria volcada justo hoy por la mañana.

Tenemos un proyecto que opera una gran cantidad de datos (hasta 2 GB). Como almacenamiento principal utilizamos Dictionary . En realidad, se crean miles de diccionarios. Después de cambiarlo a List para las claves y List para los valores (implementamos IDictionary ourselves) el uso de la memoria disminuyó en aproximadamente 30-40%.

¿Por qué?