Qual é o tamanho de um booleano em c #? Realmente leva 4 bytes?

137

Eu tenho duas estruturas com matrizes de bytes e 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;
}

E o seguinte 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();
    }
}

Isso me dá a seguinte saída:

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

Parece que um booleanleva 4 bytes de armazenamento. Idealmente, um boolean levaria apenas um bit ( falseou true, 0ou 1, etc.).

O que esta acontecendo aqui? O booleantipo é realmente tão ineficiente?

biv
fonte
7
Esse é um dos confrontos mais irônicos da batalha em andamento: duas excelentes respostas de John e Hans acabaram de chegar, embora as respostas a essa pergunta tendam a ser quase inteiramente baseadas em opiniões, e não em fatos, referências, ou conhecimentos específicos.
TAW
12
@TaW: Meu palpite é que os votos próximos não foram devidos às respostas, mas ao tom original do OP quando eles fizeram a pergunta pela primeira vez - eles claramente pretendiam começar uma briga e mostraram isso de maneira clara nos comentários agora excluídos. A maior parte do lixo foi varrida para debaixo do tapete, mas confira o histórico de revisões para ter uma idéia do que quero dizer.
BoltClock
1
Por que não usar um BitArray?
ded '16 de

Respostas:

242

O tipo bool tem um histórico quadriculado com muitas opções incompatíveis entre os tempos de execução do idioma. Isso começou com uma escolha histórica de design feita por Dennis Ritchie, o cara que inventou a linguagem C. Não tinha um tipo booleano , a alternativa era int em que um valor 0 representa false e qualquer outro valor foi considerado verdadeiro .

Essa opção foi levada adiante no Winapi, o principal motivo para usar o pinvoke, pois possui um typedef para o BOOLqual é um alias da palavra-chave int do compilador C. Se você não aplicar um atributo [MarshalAs] explícito, um booleano C # será convertido em BOOL, produzindo um campo com 4 bytes de comprimento.

Faça o que fizer, sua declaração de estrutura precisa corresponder à escolha do tempo de execução feita no idioma com o qual você interage. Como observado, o BOOL para o winapi, mas a maioria das implementações de C ++ escolheu o byte , a maior interoperabilidade de automação do COM usa VARIANT_BOOL, que é curto .

O tamanho real de um C # boolé um byte. Um forte objetivo de design do CLR é que você não pode descobrir. O layout é um detalhe de implementação que depende muito do processador. Os processadores são muito exigentes quanto aos tipos de variáveis ​​e alinhamento; escolhas erradas podem afetar significativamente o desempenho e causar erros de tempo de execução. Ao tornar o layout não detectável, o .NET pode fornecer um sistema de tipo universal que não depende da implementação real do tempo de execução.

Em outras palavras, você sempre precisa organizar uma estrutura em tempo de execução para definir o layout. Nesse momento, é feita a conversão do layout interno para o layout de interoperabilidade. Isso pode ser muito rápido se o layout for idêntico, lento quando os campos precisarem ser reorganizados, pois isso sempre exige a criação de uma cópia da estrutura. O termo técnico para isso é blittable , passar uma estrutura blittable para o código nativo é rápido porque o empacotador pinvoke pode simplesmente passar um ponteiro.

O desempenho também é a principal razão pela qual um bool não é um único bit. Existem poucos processadores que tornam um pouco diretamente endereçável, a menor unidade é um byte. É necessária uma instrução extra para pescar o bit fora do byte, que não é de graça. E nunca é atômico.

O compilador C # não tem vergonha de dizer que é necessário 1 byte, use sizeof(bool). Ainda não é um preditor fantástico para quantos bytes um campo leva em tempo de execução, o CLR também precisa implementar o modelo de memória .NET e promete que atualizações simples de variáveis ​​sejam atômicas . Isso requer que as variáveis ​​sejam alinhadas corretamente na memória para que o processador possa atualizá-lo com um único ciclo de barramento de memória. Com bastante frequência, um bool exige 4 ou 8 bytes de memória por causa disso. Preenchimento extra que foi adicionado para garantir que o próximo membro esteja alinhado corretamente.

O CLR realmente aproveita o fato de o layout não ser descoberto, ele pode otimizar o layout de uma classe e reorganizar os campos para que o preenchimento seja minimizado. Então, digamos, se você tem uma classe com um membro bool + int + bool, seriam necessários 1 + (3) + 4 + 1 + (3) bytes de memória, (3) é o preenchimento, para um total de 12 bytes. 50% de resíduos. O layout automático é reorganizado para 1 + 1 + (2) + 4 = 8 bytes. Somente uma classe tem layout automático, as estruturas têm layout seqüencial por padrão.

Mais sombriamente, um bool pode exigir até 32 bytes em um programa C ++ compilado com um compilador C ++ moderno que suporta o conjunto de instruções AVX. O que impõe um requisito de alinhamento de 32 bytes, a variável bool pode acabar com 31 bytes de preenchimento. Além disso, a principal razão pela qual uma instabilidade .NET não emite instruções SIMD, a menos que explicitamente agrupada, não pode obter a garantia de alinhamento.

Hans Passant
fonte
2
Para um leitor interessado, mas desinformado, você esclareceria se o último parágrafo deveria realmente ler 32 bytes e não bits ?
Silly Freak #
3
Não sei por que acabei de ler tudo isso (já que não preciso de tantos detalhes), mas isso é fascinante e bem escrito.
Frank V
2
@ Silly - são bytes . O AVX usa variáveis ​​de 512 bits para fazer cálculos em 8 valores de ponto flutuante com uma única instrução. Tal variável a 512 bits requer alinhamento a 32.
Hans Passant
3
Uau! um post deu muitos tópicos para entender. É por isso que eu gosto de ler as principais perguntas.
Chaitanya Gadkari
151

Em primeiro lugar, esse é apenas o tamanho da interoperabilidade. Não representa o tamanho no código gerenciado da matriz. São 1 byte por bool- pelo menos na minha máquina. Você pode testá-lo por si mesmo com 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); 
    } 
}

Agora, para organizar matrizes por valor, como você é, a documentação diz:

Quando a propriedade MarshalAsAttribute.Value é definida como ByValArray, o campo SizeConst deve ser definido para indicar o número de elementos na matriz. O ArraySubTypecampo pode conter, opcionalmente, a UnmanagedTypedos elementos da matriz, quando é necessário diferenciar entre tipos de cordas. Você pode usar isso UnmanagedTypesomente em uma matriz cujos elementos apareçam como campos em uma estrutura.

Então, olhamos ArraySubType, e isso tem documentação de:

Você pode definir esse parâmetro como um valor da UnmanagedTypeenumeração para especificar o tipo dos elementos da matriz. Se um tipo não for especificado, o tipo não gerenciado padrão correspondente ao tipo de elemento da matriz gerenciada será usado.

Agora, olhando UnmanagedType, há:

Bool
Um valor booleano de 4 bytes (verdadeiro! = 0, falso = 0). Este é o tipo Win32 BOOL.

Portanto, esse é o padrão para bool, e são 4 bytes porque corresponde ao tipo BO32 do Win32 - portanto, se você estiver interoperando com o código que espera uma BOOLmatriz, ele faz exatamente o que deseja.

Agora você pode especificar o ArraySubTypeas I1, que está documentado como:

Um número inteiro assinado de 1 byte. Você pode usar esse membro para transformar um valor booleano em um bool de 1 byte, estilo C (true = 1, false = 0).

Portanto, se o código com o qual você interopera espera 1 byte por valor, basta usar:

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

Seu código mostrará isso como ocupando 1 byte por valor, conforme o esperado.

Jon Skeet
fonte