Nous savons tous que le CPU et la mémoire sont les deux indicateurs les plus importants pour un programme, alors combien de personnes se sont vraiment posées la question : Combien d’octets occupe une instance d’un type (type de valeur ou type de référence) en mémoire ? Beaucoup d’entre nous ne peuvent pas répondre. C# fournit certains opérateurs et API pour calculer les tailles, mais aucun ne résout complètement le problème que je viens de poser. Cet article fournit une méthode pour calculer le nombre d’octets mémoire occupés par des instances de types de valeur et de types de référence. Le code source est téléchargé ici.
1. Taille de l’opérateur 2. Méthode Marshal. Taille 3. Dangereux. Taille de la méthode > 4. Peut-on le calculer en fonction du type de membre du terrain ? 5. Disposition des types de valeurs et types d’applications 6. Directive LDFLDA 7. Calculer le nombre d’octets du type de valeur 8. Compter le nombre d’octets du type de citation 9. Calcul complet
1. Taille de l’opérateur
L’opération sizeof est utilisée pour déterminer le nombre d’octets occupés par une instance d’un type, mais elle ne peut être appliquée qu’aux types non gérés. Le type dit non géré est limité à :
Types primitifs : Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double et Single) Type décimal Type d’énumération Type de pointeur Structs qui ne contiennent que des membres de données de type Non géré Comme son nom l’indique, un type Non géré est un type de valeur, et l’instance correspondante ne peut contenir aucune référence à l’objet géré. Si l’on définit une méthode générique comme celle-ci pour appeler l’opérateur sizeof, le paramètre générique T doit ajouter une contrainte non gérée et un tag non sûr à la méthode.
Seuls les types natifs et enum peuvent utiliser directement l’opérateur sizeof, qui doit être ajouté s’il est appliqué à d’autres types (pointeurs et structs personnalisés)./unsafeet doivent également être placés dansdangereuxdans le contexte.
Puisque la structure suivante Foobar n’est pas de type non géré, le programme aura une erreur de compilation.
2. Méthode Marshal. Taille
Types statiques Marshal définit une série d’API qui nous aident à allouer et copier la mémoire non gérée, à convertir entre types managés et non gérés, et à effectuer une série d’autres opérations sur la mémoire non gérée (Marshal en science computationnelle désigne l’opération de conversion des objets mémoire dans le format correspondant pour le stockage ou le transfert de données). Statique, qui inclut les 4 surcharges de méthode SizeOf suivantes pour déterminer le nombre d’octets d’un type ou d’un objet donné.
La méthode Marshal.SizeOf n’a pas de restriction sur le type spécifié pour le type non géré, mais elle nécessite tout de même qu’une restriction soit spécifiéeType de valeur。 Si l’objet entrant est un objet, il doit aussi être une case pour un type de valeur.
Puisque le Foobar suivant est défini comme suit :gentil, donc les appels aux deux méthodes SizeOf lanceront une exception ArgumentException et une invite : Type 'Foobar' ne peut pas être marshalé comme une structure non gérée ; aucune taille ou décalage significatif ne peut être calculé.
Méthode Marshal. TailleLes génériques ne sont pas pris en charge, mais il y a aussi des exigences pour la disposition de la structure, qui soutient le supportSéquentieletExpliciteMode mise en page. Puisque la structure Foobar présentée ci-dessous adopte le mode Auto layout (Auto, qui ne prend pas en charge la « planification dynamique » de la disposition mémoire basée sur les éléments de champ en raison des exigences plus strictes de disposition mémoire dans les environnements non gérés), les appels à la méthode SizeOf lanceront toujours la même exception ArgumentException que ci-dessus.
3. Méthode dangereuse. Taille de la méthode
Static Unsafe fournit des opérations plus bas-étendues pour la mémoire non gérée, et des méthodes similaires de SizeIOf sont également définies dans ce type. La méthode n’a aucune restriction sur le type spécifié, mais si vous spécifiez un type de référence, elle retourne leNombre d’octets pointeurs"(IntPtr.Size)。
4. Peut-on le calculer en fonction du type de membre du terrain ?
Nous savons que les types de valeur et de référence sont tous deux mappés comme un fragment continu (ou stockés directement dans un registre). Le but d’un type est de spécifier la disposition mémoire d’un objet, et les instances du même type ont la même disposition et le nombre d’octets est naturellement le même (pour les champs de type de référence, il ne stocke que l’adresse référencée dans cette séquence d’octets). Puisque la longueur de l’octet est déterminée par le type, si nous pouvons déterminer le type de chaque membre de champ, ne serions-nous pas capables de calculer le nombre d’octets correspondant à ce type ? En réalité, ce n’est pas possible.
Par exemple, nous savons que les octets d’octet, court, int et long sont 1, 2, 4 et 8, donc le nombre d’octets pour un binaire d’octets est 2, mais pour une combinaison de types octet + court, octet + int et octet + long, les octets correspondants ne sont pas 3, 5 et 9, mais 3, 8 et 16. Parce que cela implique la question de l’alignement de la mémoire.
5. Disposition des types de valeurs et des types de référence
Le nombre d’octets occupés par les instances du type et du sous-type de référence diffère également pour le même membre de données exact. Comme montré dans l’image suivante, la séquence d’octets de l’instance de type de valeurTous sont des membres de terrain utilisés pour le stocker。 Pour les instances de types de référence, l’adresse de la table de méthode correspondante est également stockée devant la séquence d’octets du champ. La table de méthodes fournit presque toutes les métadonnées décrivant le type, et nous utilisons cette référence pour déterminer à quel type appartient l’instance. Tout en avant, il y a aussi des octets supplémentaires, que nous appelleronsEn-tête d’objetIl est utilisé non seulement pour stocker l’état verrouillé de l’objet, mais la valeur de hachage peut aussi être mise en cache ici. Lorsque nous créons une variable de type de référence, cette variableIl ne pointe pas vers le premier octet de mémoire occupé par l’instance, mais vers l’endroit où l’adresse de la table de méthode est stockée。
6. Directive LDFLDA
Comme nous l’avons présenté plus haut, l’opérateur sizeof et la méthode SizeOf fournie par le type statique Marshal/Unsafe ne peuvent vraiment résoudre le calcul de la longueur d’octet occupée par les instances. À ma connaissance, ce problème ne peut pas être résolu uniquement dans le domaine C#, mais il est fourni au niveau de l’ILLDFLDADes instructions peuvent nous aider à résoudre ce problème. Comme son nom l’indique, Ldflda signifie Load Field Address, ce qui nous aide à obtenir l’adresse d’un champ dans l’instance. Puisque cette instruction IL n’a pas d’API correspondante en C#, nous ne pouvons l’utiliser que sous la forme suivante avec IL Emit.
Comme montré dans l’extrait de code ci-dessus, nous avons une méthode GenerateFieldAddressAccessor dans le type SizeCalculator, qui génère un délégué de type Func<object ?, long[]> basé sur la liste des champs du type spécifié, ce qui nous aide à retourner l’adresse mémoire de l’objet spécifié et de tous ses champs. Avec l’adresse de l’objet lui-même et l’adresse de chaque champ, nous pouvons naturellement obtenir le décalage de chaque champ, puis calculer facilement le nombre d’octets de mémoire occupés par l’ensemble de l’instance.
7. Calculer le nombre d’octets du type de valeur
Puisque les types de valeurs et les types de référence ont des dispositions différentes en mémoire, nous devons aussi utiliser des calculs différents. Puisque l’octet de la structure est le contenu de tous les champs en mémoire, nous utilisons une méthode astucieuse pour le calculer. Supposons que nous devions régler le nombre d’octets d’une struct de type T, alors nous créons un tuple ValueTuple<T,T>, et que le décalage de son second champ Item2 soit le nombre d’octets de la structure T. La méthode de calcul spécifique se reflète dans la méthode suivante, CalculateValueTypeInstance.
Comme montré dans l’extrait de code ci-dessus, en supposant que le type de struct à calculer soit T, nous appelons la méthode GetDefaultAsObject pour obtenir l’objet default(T) sous forme de réflexion, puis créons un ValueTuple<T,T>. Après avoir appelé la méthode GenerateFieldAddressAccessor pour obtenir le délégué Func<object ?, long[]> pour calculer l’instance et ses adresses de champs, nous appelons ce délégué en tant qu’argument. Pour les trois adresses mémoire obtenues, le tuple de code et les adresses des champs 1 et 2 sont les mêmes ; on utilise la troisième adresse représentant l’Item2 moins la première, et on obtient le résultat souhaité.
8. Compter le nombre d’octets du type de citation
Le calcul des octets pour les types de référence est plus complexe, en utilisant cette idée : après avoir obtenu l’adresse de l’instance elle-même et de chaque champ, nous trions les adresses pour obtenir le décalage du dernier champ. Ajoutons ce décalage au nombre d’octets du dernier champ lui-même, puis ajoutons le « premier et dernier octet » nécessaires au résultat souhaité, ce qui se reflète dans la méthode suivante CalculateReferneceTypeInstance.
Comme montré dans l’extrait de code ci-dessus, si le type spécifié n’a aucun champ défini, CalculateReferneceTypeInstance retourne le nombre minimum d’octets de l’instance du type de référence : 3 fois le nombre d’octets de pointeurs d’adresse. Pour les architectures x86, un objet type application occupe au moins 12 octets, incluant ObjectHeader (4 octets), les pointeurs de la table de méthode (octets) et au moins 4 octets de contenu champ (ces 4 octets sont nécessaires même si aucun type n’est défini sans champs). Dans le cas de l’architecture x64, ce nombre minimum d’octets sera de 24, car le pointeur de la table de la méthode et le contenu minimum du champ deviendront 8 octets, bien que le contenu valide de l’ObjectHeader n’occupe que 4 octets, mais 4 octets de bourrage seront ajoutés devant.
L’attribution des octets occupés par le dernier champ est également très simple : si le type est un type de valeur, alors la méthode CalculateValueTypeInstance définie précédemment est appelée pour calculer ; si c’est un type de référence, le contenu stocké dans le champ n’est que l’adresse mémoire de l’objet cible, donc la longueur est IntPtr.Size. Puisque les instances de type de référence sont alignées par défaut avec IntPtr.Size en mémoire, cela se fait également ici. Enfin, n’oubliez pas que la référence de l’instance de type référence ne pointe pas vers le premier octet de mémoire, mais vers l’octet qui stocke le pointeur de la table de méthodes, donc il faut ajouter le nombre d’octets d’ObjecthHeader (IntPtr.Size).
9. Calcul complet
Les deux méthodes utilisées pour calculer le nombre d’octets d’instances de type de valeur et de type de référence sont utilisées dans la méthode SizeOf suivante. Puisque l’appel de l’instruction Ldflda doit fournir une instance correspondante, cette méthode fournit un délégué pour obtenir l’instance correspondante en plus de fournir le type cible. Les paramètres correspondant à ce délégué peuvent être par défaut, et nous utiliserons la valeur par défaut pour le type de valeur. Pour les types de référence, nous allons aussi essayer de créer l’objet cible en utilisant le constructeur par défaut. Si cet objet délégué n’est pas fourni et que l’instance cible ne peut pas être créée, la méthode SizeOf lance une exception. Bien que nous devions fournir l’instance cible, le résultat calculé n’est lié qu’au type, donc nous mettons en cache le résultat calculé. Pour faciliter l’appel, nous proposons également une autre méthode générique de type <T>SizeOf.
Dans le extrait de code ci-dessous, nous l’utilisons pour afficher le nombre d’octets de deux structs et types avec la même définition de champ. Dans le prochain article, nous obtiendrons également le contenu binaire complet de l’instance en mémoire basé sur le nombre calculé d’octets, alors restez à l’écoute.
Lien original :La connexion hyperlientérée est visible. |