Todos sabemos que la CPU y la memoria son las dos métricas más importantes para un programa, así que ¿cuántas personas han pensado realmente en la pregunta: ¿Cuántos bytes ocupa una instancia de un tipo (tipo valor o tipo de referencia) en la memoria? Muchos de nosotros no podemos responder. C# proporciona algunos operadores y APIs para calcular tamaños, pero ninguno resuelve completamente el problema que acabo de preguntar. Este artículo proporciona un método para calcular el número de bytes de memoria ocupados por instancias de tipos de valor y tipos de referencia. El código fuente se descarga aquí.
1. Tamaño del operador 2. Marshal. Método SizeOf 3. Inseguro. Tamaño del método > 4. ¿Se puede calcular en función del tipo de miembro del campo? 5. Distribución de tipos de valores y tipos de aplicación 6. Directiva LDFLDA 7. Calcular el número de bytes del tipo de valor 8. Contar el número de bytes del tipo de cita 9. Cálculo completo
1. Tamaño del operador
La operación sizeof se utiliza para determinar el número de bytes ocupados por una instancia de un tipo, pero solo puede aplicarse a tipos no gestionados. El llamado tipo No gestionado está limitado a:
Tipos primitivos: booleano, byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double y Single) Tipo decimal Tipo de enumeración Tipo de puntero Estructuras que contienen solo miembros de datos del tipo No gestionado Como su nombre indica, un tipo No gestionado es un tipo de valor, y la instancia correspondiente no puede contener ninguna referencia al objeto gestionado. Si definimos un método genérico como este para llamar al operador sizeof, el parámetro genérico T debe añadir una restricción no gestionada y una etiqueta insegura al método.
Solo los tipos nativos y enum pueden usar directamente el operador sizeof, que debe añadirse si se aplica a otros tipos (punteros y estructuras personalizadas)./unsafeetiquetas de compilación, y también deben colocarse eninseguroen contexto.
Dado que la siguiente estructura Foobar no es un tipo No gestionado, el programa tendrá un error de compilación.
2. Marshal. Método SizeOf
Tipos estáticos Marshal define una serie de APIs que nos ayudan a asignar y copiar memoria no gestionada, convertir entre tipos gestionados y no gestionados, y realizar una serie de otras operaciones sobre memoria no gestionada (Marshal en ciencia computacional se refiere a la operación de convertir objetos de memoria en el formato correspondiente para almacenamiento o transferencia de datos). Estático, que incluye las siguientes 4 sobrecargas de métodos SizeOf para determinar el número de bytes de un tipo u objeto dado.
El método Marshal.SizeOf no tiene una restricción sobre el tipo especificado para el tipo No gestionado, pero aún así requiere que se especifique unaTipo de valor。 Si el objeto entrante es un objeto, también debe ser una caja para un tipo de valor.
Dado que el siguiente Foobar se define como:amable, por lo que las llamadas a ambos métodos SizeOf lanzarán una excepción ArgumentException y un prompt: El tipo 'Foobar' no puede ser marshalizado como una estructura no gestionada; no se puede calcular un tamaño o desplazamiento significativo.
Método Marshal.SizeOfNo se admiten genéricos, pero también tiene requisitos para la disposición de la estructura, que soporta soporteSecuencialyExplícitoModo de distribución. Dado que la estructura Foobar mostrada a continuación adopta el modo de disposición Auto (Auto, que no soporta "planificación dinámica" de la disposición de memoria basada en miembros de campo debido a los requisitos más estrictos de disposición de memoria en entornos no gestionados), las llamadas al método SizeOf seguirán generando la misma excepción ArgumentException que antes.
3. Método inseguro. TamañoDeMétodo
Static Unsafe proporciona operaciones de bajo nivel más para memoria no gestionada, y métodos similares de SizeIOf también se definen en este tipo. El método no tiene restricciones sobre el tipo especificado, pero si especificas un tipo de referencia, devuelve elNúmero de bytes de puntero"(Tamaño IntPtr)。
4. ¿Se puede calcular en función del tipo de miembro del campo?
Sabemos que tanto los tipos de valor como los de referencia se mapean como un fragmento continuo (o se almacenan directamente en un registro). El propósito de un tipo es especificar la disposición de memoria de un objeto, y las instancias del mismo tipo tienen la misma disposición y el número de bytes es naturalmente el mismo (para campos de tipo de referencia, almacena solo la dirección referenciada en esta secuencia de bytes). Dado que la longitud del byte se determina por el tipo, si podemos determinar el tipo de cada miembro del campo, ¿no podríamos calcular el número de bytes correspondientes a ese tipo? De hecho, no es posible.
Por ejemplo, sabemos que los bytes de byte, short, int y long son 1, 2, 4 y 8, así que el número de bytes para un binario de bytes es 2, pero para una combinación de tipos de byte + short, byte + int y byte + long, los bytes correspondientes no son 3, 5 y 9, sino 3, 8 y 16. Porque esto implica el tema de la alineación de la memoria.
5. Disposición de los tipos de valor y los tipos de referencia
El número de bytes ocupados por instancias del tipo y subtipo de referencia también es diferente para el mismo miembro exacto de los datos. Como se muestra en la siguiente imagen, la secuencia de bytes del tipo de valor de instanciaTodos son miembros de campo que se usan para almacenarlo。 Para instancias de tipos de referencia, la dirección de la tabla de métodos correspondiente al tipo también se almacena delante de la secuencia de bytes de campo. La tabla de métodos proporciona casi todos los metadatos que describen el tipo, y usamos esta referencia para determinar a qué tipo pertenece la instancia. En la parte más frontal también hay bytes extra, que llamaremosCabecera de objetoNo solo se utiliza para almacenar el estado bloqueado del objeto, sino que el valor hash también puede almacenarse en caché aquí. Cuando creamos una variable de tipo de referencia, esta variableNo apunta al primer byte de memoria ocupado por la instancia, sino al lugar donde se almacena la dirección de la tabla de métodos。
6. Directiva LDFLDA
Como hemos introducido anteriormente, el operador sizeof y el método SizeOf proporcionados por el tipo estático Marshal/Unsafe no pueden resolver realmente el cálculo de la longitud de byte ocupada por instancias. Hasta donde yo sé, este problema no se puede resolver solo en el campo de C#, pero se proporciona a nivel de ILLDFLDALas instrucciones pueden ayudarnos a resolver este problema. Como su nombre indica, Ldflda significa Load Field Address, lo que nos ayuda a obtener la dirección de un campo en la instancia. Dado que esta instrucción IL no tiene una API correspondiente en C#, solo podemos usarla en la siguiente forma usando IL Emit.
Como se muestra en el fragmento de código anterior, tenemos un método GenerateFieldAddressAccessor en el tipo SizeCalculator, que genera un delegado de tipo Func<object?, long[]> basado en la lista de campos del tipo especificado, lo que nos ayuda a devolver la dirección de memoria del objeto especificado y todos sus campos. Con la dirección del objeto en sí y la dirección de cada campo, podemos obtener naturalmente el desplazamiento de cada campo y calcular fácilmente el número de bytes de memoria ocupados por toda la instancia.
7. Calcular el número de bytes del tipo de valor
Dado que los tipos de valor y los tipos de referencia tienen diferentes disposiciones en la memoria, también necesitamos usar cálculos distintos. Como el byte de la estructura es el contenido de todos los campos en la memoria, usamos una forma ingeniosa de calcularlo. Supongamos que necesitamos ajustar el número de bytes de una estructura de tipo T, creamos una tupla ValueTuple<T,T>, y el desplazamiento de su segundo campo Elemento2 es el número de bytes de la estructura T. El método de cálculo específico se refleja en el siguiente método CalculateValueTypeInstance.
Como se muestra en el fragmento de código anterior, asumiendo que el tipo de struct que necesitamos calcular es T, llamamos al método GetDefaultAsObject para obtener el objeto default(T) en forma de reflexión, y luego creamos un ValueTuple<T,T>tuple. Después de llamar al método GenerateFieldAddressAccessor para obtener el delegado Func<object?, long[]> para calcular la instancia y sus direcciones de campo, llamamos a este delegado como argumento. Para las tres direcciones de memoria que obtenemos, la tupla de código y las direcciones de los campos 1 y 2 son las mismas, usamos la tercera dirección que representa el Item2 menos la primera dirección, y obtenemos el resultado que queremos.
8. Contar el número de bytes del tipo de cita
El cálculo de bytes para tipos de referencia es más complicado usando esta idea: después de obtener la dirección de la instancia y de cada campo, ordenamos las direcciones para obtener el desplazamiento del último campo. Sumemos este desplazamiento al número de bytes del último campo, y luego sumemos el "primer y último bytes" necesarios al resultado que queremos, lo cual se refleja en el siguiente método CalculateReferneceTypeInstance.
Como se muestra en el fragmento de código anterior, si el tipo especificado no tiene campos definidos, CalculateReferneceTypeInstance devuelve el número mínimo de bytes de la instancia del tipo de referencia: 3 veces el número de bytes de puntero de dirección. Para arquitecturas x86, un objeto tipo aplicación ocupa al menos 12 bytes, incluyendo ObjectHeader (4 bytes), punteros de la tabla de métodos (bytes) y al menos 4 bytes de contenido de campo (estos 4 bytes son necesarios incluso si ningún tipo está definido sin ningún campo). En el caso de la arquitectura x64, este número mínimo de bytes será 24, porque el puntero de la tabla de métodos y el contenido mínimo del campo serán 8 bytes, aunque el contenido válido del ObjectHeader solo ocupa 4 bytes, pero se añadirán 4 bytes de relleno al frente.
El asentamiento de bytes ocupados por el último campo también es muy sencillo: si el tipo es un tipo de valor, entonces se llama al método CalculateValueTypeInstance definido anteriormente para calcular; si es un tipo de referencia, el contenido almacenado en el campo es solo la dirección de memoria del objeto destino, por lo que la longitud es IntPtr.Size. Dado que las instancias de tipo referencia están alineadas por defecto con IntPtr.Size en memoria, esto también se hace aquí. Por último, no olvides que la referencia de la instancia de tipo de referencia no apunta al primer byte de memoria, sino al byte que almacena el puntero de la tabla de métodos, así que tienes que sumar el número de bytes de ObjecthHeader (IntPtr.Size).
9. Cálculo completo
Los dos métodos usados para calcular el número de bytes de instancias de tipo valor y tipo de referencia se emplean en el siguiente método SizeOf. Dado que la llamada de la instrucción Ldflda debe proporcionar una instancia correspondiente, este método proporciona un delegado para obtener la instancia correspondiente además de proporcionar el tipo objetivo. Los parámetros correspondientes a este delegado pueden ser predeterminados, y usaremos el valor por defecto para el tipo de valor. Para los tipos de referencia, también intentaremos crear el objeto destino usando el constructor por defecto. Si este objeto delegado no se proporciona y no se puede crear la instancia destino, el método SizeOf lanza una excepción. Aunque necesitamos proporcionar la instancia objetivo, el resultado calculado solo está relacionado con el tipo, así que almacenamos en caché el resultado calculado. Para facilitar la llamada, también ofrecemos otro método genérico de <T>SizeOf.
En el fragmento de código a continuación, lo usamos para mostrar el número de bytes de dos structs y tipos con la misma definición de campo. En el próximo artículo, obtendremos además el contenido binario completo de la instancia en memoria basado en el número calculado de bytes, así que estad atentos.
Enlace original:El inicio de sesión del hipervínculo es visible. |