¿Cuál es el tamaño de un booleano en C #? ¿Realmente toma 4 bytes?

137

Tengo dos estructuras con matrices de bytes y booleanos:

using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct struct1
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public byte[] values;
}

[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct struct2
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public bool[] values;
}

Y el siguiente código:

class main
{
    public static void Main()
    {
        Console.WriteLine("sizeof array of bytes: "+Marshal.SizeOf(typeof(struct1)));
        Console.WriteLine("sizeof array of bools: " + Marshal.SizeOf(typeof(struct2)));
        Console.ReadKey();
    }
}

Eso me da el siguiente resultado:

sizeof array of bytes: 3
sizeof array of bools: 12

Parece ser que booleantoma 4 bytes de almacenamiento. Idealmente boolean , a solo tomaría un bit ( falseo true, 0o 1, etc.).

¿Que está sucediendo aquí? ¿El booleantipo es realmente tan ineficiente?

biv
fuente
77
Este es uno de los enfrentamientos más irónicos en la batalla en curso de las razones de retención: dos excelentes respuestas de John y Hans lo lograron, a pesar de que las respuestas a esta pregunta tenderán a basarse casi por completo en opiniones, en lugar de hechos, referencias, o experiencia específica.
TaW
12
@TaW: Supongo que los votos cerrados no se debieron a las respuestas, sino al tono original del OP cuando plantearon la pregunta por primera vez: claramente tenían la intención de comenzar una pelea y lo demostraron directamente en los comentarios ahora eliminados. La mayor parte de la ruina ha sido barrida debajo de la alfombra, pero revise el historial de revisiones para tener una idea de lo que quiero decir.
BoltClock
1
¿Por qué no usar un BitArray?
ded '16 de

Respuestas:

242

El tipo bool tiene un historial a cuadros con muchas opciones incompatibles entre los tiempos de ejecución del idioma. Esto comenzó con una elección histórica de diseño hecha por Dennis Ritchie, el tipo que inventó el lenguaje C. No tenía un tipo bool , la alternativa era int donde un valor de 0 representa falso y cualquier otro valor se considera verdadero .

Esta elección se llevó a cabo en Winapi, la razón principal para usar pinvoke, tiene un typedef para el BOOLcual es un alias para la palabra clave int del compilador de C. Si no aplica un atributo explícito [MarshalAs], un bool de C # se convierte en BOOL, lo que genera un campo de 4 bytes de longitud.

Independientemente de lo que haga, su declaración de estructura debe coincidir con la elección de tiempo de ejecución realizada en el idioma con el que interopera. Como se señaló, BOOL para el winapi, pero la mayoría de las implementaciones de C ++ eligieron el byte , la mayoría de la interoperabilidad de COM utiliza VARIANT_BOOL, que es una abreviatura .

El tamaño real de un C # booles un byte. Un fuerte objetivo de diseño del CLR es que no puede descubrirlo. El diseño es un detalle de implementación que depende demasiado del procesador. Los procesadores son muy exigentes con los tipos de variables y la alineación, las elecciones incorrectas pueden afectar significativamente el rendimiento y causar errores de tiempo de ejecución. Al hacer que el diseño no se pueda descubrir, .NET puede proporcionar un sistema de tipo universal que no depende de la implementación real del tiempo de ejecución.

En otras palabras, siempre debe ordenar una estructura en tiempo de ejecución para definir el diseño. En ese momento se realiza la conversión del diseño interno al diseño de interoperabilidad. Eso puede ser muy rápido si el diseño es idéntico, lento cuando los campos necesitan ser reorganizados ya que eso siempre requiere crear una copia de la estructura. El término técnico para esto es blittable , pasar una estructura blittable a código nativo es rápido porque el jefe de orquestación pinvoke simplemente puede pasar un puntero.

El rendimiento también es la razón principal por la cual un bool no es un solo bit. Hay pocos procesadores que hacen que un poco sea directamente direccionable, la unidad más pequeña es un byte. Se requiere una instrucción adicional para extraer un poco del byte, que no es gratis. Y nunca es atómico.

El compilador de C # no es tímido al decirle que toma 1 byte, use sizeof(bool). Este aún no es un predictor fantástico de cuántos bytes ocupa un campo en tiempo de ejecución, el CLR también necesita implementar el modelo de memoria .NET y promete que las actualizaciones simples de variables son atómicas . Eso requiere que las variables estén correctamente alineadas en la memoria para que el procesador pueda actualizarlo con un solo ciclo de bus de memoria. Muy a menudo, un bool realmente requiere 4 u 8 bytes en memoria debido a esto. Acolchado adicional que se agregó para garantizar que el siguiente miembro esté alineado correctamente.

El CLR realmente aprovecha el diseño que no se puede descubrir, puede optimizar el diseño de una clase y reorganizar los campos para minimizar el relleno. Entonces, digamos, si tiene una clase con un miembro bool + int + bool, entonces tomaría 1 + (3) + 4 + 1 + (3) bytes de memoria, (3) es el relleno, para un total de 12 bytes 50% de residuos. El diseño automático se reorganiza a 1 + 1 + (2) + 4 = 8 bytes. Solo una clase tiene un diseño automático, las estructuras tienen un diseño secuencial por defecto.

Más sombríamente, un bool puede requerir hasta 32 bytes en un programa C ++ compilado con un compilador moderno de C ++ que admite el conjunto de instrucciones AVX. Lo que impone un requisito de alineación de 32 bytes, la variable bool puede terminar con 31 bytes de relleno. También es la razón principal por la cual un jitter de .NET no emite instrucciones SIMD, a menos que se envuelva explícitamente, no puede obtener la garantía de alineación.

Hans Passant
fuente
2
Para un lector interesado pero desinformado, ¿aclararía si el último párrafo realmente debería leer 32 bytes y no bits ?
Silly Freak
3
No estoy seguro de por qué acabo de leer todo esto (ya que no necesito tantos detalles), pero eso es fascinante y está bien escrito.
Frank V
2
@Silly: son bytes . AVX utiliza variables de 512 bits para hacer cálculos matemáticos en 8 valores de coma flotante con una sola instrucción. Tal variable de 512 bits requiere alineación a 32.
Hans Passant
3
¡Guauu! Una publicación dio muchos temas para entender. Es por eso que me gusta leer las principales preguntas.
Chaitanya Gadkari
151

En primer lugar, este es solo el tamaño para interoperabilidad. No representa el tamaño en el código administrado de la matriz. Eso es 1 byte por bool- al menos en mi máquina. Puede probarlo usted mismo con este código:

using System;
class Program 
{ 
    static void Main(string[] args) 
    { 
        int size = 10000000;
        object array = null;
        long before = GC.GetTotalMemory(true); 
        array = new bool[size];
        long after = GC.GetTotalMemory(true); 

        double diff = after - before; 

        Console.WriteLine("Per value: " + diff / size);

        // Stop the GC from messing up our measurements 
        GC.KeepAlive(array); 
    } 
}

Ahora, para ordenar matrices por valor, tal como eres, la documentación dice:

Cuando la propiedad MarshalAsAttribute.Value se establece en ByValArray, el campo SizeConst debe establecerse para indicar el número de elementos en la matriz. El ArraySubTypecampo puede contener opcionalmente los UnmanagedTypeelementos de la matriz cuando sea necesario diferenciar entre los tipos de cadena. Puede usar esto UnmanagedTypesolo en una matriz cuyos elementos aparecen como campos en una estructura.

Entonces miramos ArraySubType, y eso tiene documentación de:

Puede establecer este parámetro en un valor de la UnmanagedTypeenumeración para especificar el tipo de elementos de la matriz. Si no se especifica un tipo, se utiliza el tipo no administrado predeterminado correspondiente al tipo de elemento de la matriz administrada.

Ahora mirando UnmanagedType, hay:

Bool
Un valor booleano de 4 bytes (verdadero! = 0, falso = 0). Este es el tipo Win32 BOOL.

Entonces, ese es el valor predeterminado para bool, y es de 4 bytes porque corresponde al tipo BOOL de Win32, por lo que si interopera con un código que espera una BOOLmatriz, hace exactamente lo que desea.

Ahora puede especificar el ArraySubTypeas en su I1lugar, que se documenta como:

Un entero con signo de 1 byte. Puede usar este miembro para transformar un valor booleano en un bool de estilo C de 1 byte (verdadero = 1, falso = 0).

Entonces, si el código con el que interopera espera 1 byte por valor, simplemente use:

[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3, ArraySubType = UnmanagedType.I1)]
public bool[] values;

Su código luego lo mostrará como ocupando 1 byte por valor, como se esperaba.

Jon Skeet
fuente