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 boolean
prend 4 octets de stockage. Idéalement, un boolean
ne prendrait qu'un bit ( false
ou true
, 0
ou 1
, etc.).
Que se passe-t-il ici? Le boolean
type est-il vraiment si inefficace?
Réponses:
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
BOOL
lequel 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 #
bool
est 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.
la source
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:Maintenant, pour rassembler les tableaux par valeur, comme vous l'êtes, la documentation dit:
Nous regardons donc
ArraySubType
, et cela a de la documentation sur:Maintenant, en regardant
UnmanagedType
, il y a: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 unBOOL
tableau, il fait exactement ce que vous voulez.Vous pouvez maintenant spécifier le
ArraySubType
as à laI1
place, qui est documenté comme:Donc, si le code avec lequel vous interagissez attend 1 octet par valeur, utilisez simplement:
Votre code montrera alors que prenant 1 octet par valeur, comme prévu.
la source