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 la sortie suivante:
sizeof array of bytes: 3
sizeof array of bools: 12
Il semble que boolean
prenne 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 type boolean
est-il vraiment si inefficace?
Le type bool a un historique en damier avec de nombreux choix incompatibles entre les exécutions linguistiques. Cela a commencé avec un choix de conception historique fait par Dennis Ritchie, le gars qui a inventé le langage C. Il n'avait pas de type bool, l'alternative était int où une valeur de 0 représente false et toute autre valeur était considérée vrai.
Ce choix a été reporté dans Winapi, la principale raison d'utiliser pinvoke, il a un typedef pour BOOL
qui est un alias du mot clé int du compilateur C. Si vous n'appliquez pas d'attribut [MarshalAs] explicite, un C # bool est converti en BOOL, produisant ainsi un champ de 4 octets de long.
Quoi que vous fassiez, votre déclaration de structure doit être en adéquation avec le choix d’exécution effectué dans la langue avec laquelle vous vous connectez. Comme indiqué précédemment, BOOL pour Winapi mais la plupart des implémentations C++ ont choisi octet, la plupart des interopérabilité COM Automation utilisent VARIANT_BOOL, qui est un bref.
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 présentation 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 afin de bien définir la présentation. A ce moment, la conversion de la disposition interne en disposition interop est effectuée. Cela peut être très rapide si la mise en page est identique, lent lorsque les champs doivent être réorganisés car cela nécessite toujours de créer une copie de la structure. Le terme technique utilisé pour cela est blittable, le passage d'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 bool n'est pas un bit. Il y a peu de processeurs qui rendent un bit directement adressable, la plus petite unité est un octet. Une instruction extra est nécessaire pour extraire le bit de l'octet, ce qui n'est pas gratuit. Et ce n'est jamais atomique.
Le compilateur C # ne craint pas autrement de vous dire qu'il faut un octet, utilisez sizeof(bool)
. Ce n'est toujours pas un prédicteur fantastique pour le nombre d'octets d'un champ à l'exécution, le CLR doit également implémenter le modèle de mémoire .NET et il promet que de simples mises à jour de variables sont atomic. Cela nécessite que les variables soient correctement alignées en mémoire afin que le processeur puisse les mettre à jour avec un seul cycle de bus de mémoire. Assez souvent, un bool nécessite en fait 4 ou 8 octets en mémoire à cause de cela. Rembourrage supplémentaire ajouté pour garantir que le membre next est correctement aligné.
Le CLR tire réellement parti du fait que la disposition est introuvable, il peut optimiser la disposition d'une classe et réorganiser les champs afin de minimiser le remplissage. Donc, disons, si vous avez une classe avec un membre bool + int + bool, alors il faudrait 1 + (3) + 4 + 1 + (3) octets de mémoire, (3) est le padding, pour un total de 12 octets. 50% de déchets. La disposition automatique est réorganisée 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 sombre, un bool peut nécessiter jusqu'à 32 octets dans un programme C++ compilé avec un compilateur C++ moderne prenant en charge le jeu d'instructions AVX. Ce qui impose une exigence d’alignement de 32 octets, la variable bool peut se retrouver avec 31 octets de remplissage. De plus, la raison principale pour laquelle une gigue .NET n'émet pas d'instructions SIMD, à moins d'être explicitement enveloppée, ne peut pas obtenir la garantie d'alignement.
Tout d’abord, c’est seulement la taille pour interop. Cela 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 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 trier les tableaux par valeur, comme vous êtes, le 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 champArraySubType
peut éventuellement contenir leUnmanagedType
des éléments du tableau lorsqu'il est nécessaire de différencier les types de chaînes. Vous pouvez utiliser ceUnmanagedType
uniquement sur un tableau dont les éléments apparaissent en tant que champs dans une structure.
Nous regardons donc ArraySubType
, et cela contient la documentation de:
Vous pouvez définir ce paramètre sur une valeur de l'énumération
UnmanagedType
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é.
Regardons maintenant UnmanagedType
, il y a:
Bool
Valeur booléenne de 4 octets (true! = 0, false = 0). C'est le type Win32 BOOL.
Donc, c'est la valeur par défaut pour bool
, et il s'agit de 4 octets, car cela correspond au type Win32 BOOL. Ainsi, si vous interagissez avec du code qui attend un tableau BOOL
, il fait exactement ce que vous voulez.
Maintenant, vous pouvez spécifier le ArraySubType
comme I1
à la place, documentée comme suit:
Un entier signé sur 1 octet. Vous pouvez utiliser ce membre pour transformer une valeur booléenne en un booléen de style C sur 1 octet (true = 1, false = 0).
Donc, si le code avec lequel vous interagissez attend un octet par valeur, utilisez simplement:
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3, ArraySubType = UnmanagedType.I1)]
public bool[] values;
Votre code montrera alors que comme occupant 1 octet par valeur, comme prévu.