Quelle est la taille d'un booléen en C #? Cela prend-il vraiment 4 octets?

137

J'ai deux structures avec des tableaux d'octets et de booléens:

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;
}

Et le code suivant:

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

Cela me donne le résultat suivant:

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

Il semble que a booleanprend 4 octets de stockage. Idéalement, un boolean ne prendrait qu'un bit ( falseou true, 0ou 1, etc.).

Que se passe-t-il ici? Le booleantype est-il vraiment si inefficace?

bivouac
la source
7
C'est l'un des affrontements les plus ironiques dans la bataille en cours des raisons de retenue: deux excellentes réponses de John et Hans viennent de le faire, même si les réponses à cette question auront tendance à être presque entièrement basées sur des opinions, plutôt que sur des faits, des références, ou une expertise spécifique.
TaW
12
@TaW: Je suppose que les votes serrés n'étaient pas dus aux réponses mais au ton original du PO lorsqu'ils ont posé la question pour la première fois - ils avaient clairement l'intention de commencer une bagarre et l'ont carrément montré dans les commentaires maintenant supprimés. La plupart de la cruauté a été balayée sous le tapis, mais consultez l'historique des révisions pour avoir un aperçu de ce que je veux dire.
BoltClock
1
Pourquoi ne pas utiliser un BitArray?
déd le

Réponses:

242

Le type booléen a un historique en damier avec de nombreux choix incompatibles entre les environnements d'exécution du langage. Cela a commencé avec un choix de conception historique fait par Dennis Ritchie, le type qui a inventé le langage C. Il n'avait pas de type booléen , l'alternative était int où une valeur de 0 représente false et toute autre valeur était considérée comme vraie .

Ce choix a été reporté dans le Winapi, la principale raison d'utiliser pinvoke, il a un typedef pour BOOLlequel est un alias pour le mot-clé int du compilateur C. Si vous n'appliquez pas d'attribut [MarshalAs] explicite, un bool C # est converti en BOOL, produisant ainsi un champ de 4 octets.

Quoi que vous fassiez, votre déclaration struct doit correspondre au choix d'exécution effectué dans la langue avec laquelle vous interopérez. Comme indiqué, BOOL pour winapi mais la plupart des implémentations C ++ ont choisi byte , la plupart des interopérations COM Automation utilisent VARIANT_BOOL qui est un court .

La taille réelle d'un C # boolest d'un octet. Un objectif de conception fort du CLR est que vous ne pouvez pas le savoir. La mise en page est un détail d'implémentation qui dépend trop du processeur. Les processeurs sont très pointilleux sur les types de variables et l'alignement, de mauvais choix peuvent affecter considérablement les performances et provoquer des erreurs d'exécution. En rendant la mise en page impossible à découvrir, .NET peut fournir un système de type universel qui ne dépend pas de l'implémentation d'exécution réelle.

En d'autres termes, vous devez toujours organiser une structure au moment de l'exécution pour définir la mise en page. À ce moment, la conversion de la disposition interne à la disposition d'interopérabilité est effectuée. Cela peut être très rapide si la mise en page est identique, lente lorsque les champs doivent être réorganisés car cela nécessite toujours de créer une copie de la structure. Le terme technique pour cela est blittable , passer une structure blittable au code natif est rapide car le marshaller pinvoke peut simplement passer un pointeur.

Les performances sont également la raison principale pour laquelle un booléen n'est pas un seul bit. Il y a peu de processeurs qui rendent un bit directement adressable, la plus petite unité est un octet. Une instruction supplémentaire est nécessaire pour extraire le bit de l'octet, cela n'est pas gratuit. Et ce n'est jamais atomique.

Le compilateur C # n'hésite pas autrement à vous dire qu'il prend 1 octet, utilisez sizeof(bool). Ce n'est toujours pas un prédicteur fantastique du nombre d'octets qu'un champ prend à l'exécution, le CLR doit également implémenter le modèle de mémoire .NET et il promet que les mises à jour de variables simples sont atomiques . Cela nécessite que les variables soient correctement alignées en mémoire afin que le processeur puisse la mettre à jour avec un seul cycle de bus mémoire. Assez souvent, un booléen nécessite en fait 4 ou 8 octets en mémoire à cause de cela. Rembourrage supplémentaire qui a été ajouté pour garantir que le membre suivant est correctement aligné.

Le CLR profite en fait du fait que la mise en page ne peut pas être découverte, il peut optimiser la mise en page d'une classe et réorganiser les champs afin que le remplissage soit minimisé. Donc, disons, si vous avez une classe avec un membre bool + int + bool, cela prendrait 1 + (3) + 4 + 1 + (3) octets de mémoire, (3) est le remplissage, pour un total de 12 octets. 50% de déchets. La disposition automatique se réorganise en 1 + 1 + (2) + 4 = 8 octets. Seule une classe a une disposition automatique, les structures ont une disposition séquentielle par défaut.

Plus sombrement, un bool peut nécessiter jusqu'à 32 octets dans un programme C ++ compilé avec un compilateur C ++ moderne qui prend en charge les jeu d'instructions AVX. Ce qui impose une exigence d'alignement de 32 octets, la variable booléenne peut se retrouver avec 31 octets de remplissage. Aussi la raison principale pour laquelle une gigue .NET n'émet pas d'instructions SIMD, sauf si elle est explicitement enveloppée, elle ne peut pas obtenir la garantie d'alignement.

Hans Passant
la source
2
Pour un lecteur intéressé mais non informé, pourriez-vous préciser si le dernier paragraphe doit vraiment lire 32 octets et non bits ?
Silly Freak
3
Je ne sais pas pourquoi je viens de lire tout cela (car je n'ai pas besoin de beaucoup de détails) mais c'est fascinant et bien écrit.
Frank V
2
@Silly - ce sont des octets . AVX utilise des variables de 512 bits pour faire des calculs sur 8 valeurs à virgule flottante avec une seule instruction. Une telle variable de 512 bits nécessite un alignement sur 32.
Hans Passant
3
Hou la la! un article a donné beaucoup de sujets à comprendre. C'est pourquoi j'aime lire les principales questions.
Chaitanya Gadkari
151

Premièrement, ce n'est que la taille pour l'interopérabilité. Il ne représente pas la taille en code managé du tableau. C'est 1 octet par bool- au moins sur ma machine. Vous pouvez le tester par vous-même avec ce code:

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); 
    } 
}

Maintenant, pour rassembler les tableaux par valeur, comme vous l'êtes, la documentation dit:

Lorsque la propriété MarshalAsAttribute.Value est définie sur ByValArray, le champ SizeConst doit être défini pour indiquer le nombre d'éléments dans le tableau. Le ArraySubTypechamp peut éventuellement contenir les UnmanagedTypeéléments du tableau lorsqu'il est nécessaire de différencier les types de chaîne. Vous ne pouvez l'utiliser UnmanagedTypeque sur un tableau dont les éléments apparaissent sous forme de champs dans une structure.

Nous regardons donc ArraySubType, et cela a de la documentation sur:

Vous pouvez définir ce paramètre sur une valeur de l' UnmanagedTypeénumération pour spécifier le type des éléments du tableau. Si aucun type n'est spécifié, le type non géré par défaut correspondant au type d'élément du tableau géré est utilisé.

Maintenant, en regardant UnmanagedType, il y a:

Bool
Une valeur booléenne de 4 octets (vrai! = 0, faux = 0). Il s'agit du type Win32 BOOL.

C'est donc la valeur par défaut pour bool, et c'est 4 octets car cela correspond au type Win32 BOOL - donc si vous interagissez avec du code qui attend un BOOLtableau, il fait exactement ce que vous voulez.

Vous pouvez maintenant spécifier le ArraySubTypeas à la I1place, qui est documenté comme:

Un entier signé de 1 octet. Vous pouvez utiliser ce membre pour transformer une valeur booléenne en une valeur booléenne de type C de 1 octet (vrai = 1, faux = 0).

Donc, si le code avec lequel vous interagissez attend 1 octet par valeur, utilisez simplement:

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

Votre code montrera alors que prenant 1 octet par valeur, comme prévu.

Jon Skeet
la source