Pourquoi l’alignement struct dépend-il du type de champ, primitif ou défini par l’utilisateur?

Dans Noda Time v2, nous passons à une résolution en nanosecondes. Cela signifie que nous ne pouvons plus utiliser un entier de 8 octets pour représenter toute la plage de temps qui nous intéresse. Cela m’a incité à étudier l’utilisation de la mémoire des (nombreuses) structures de Noda Time, ce qui m’a conduit pour découvrir une légère bizarrerie dans la décision d’alignement du CLR.

Premièrement, je me rends compte que ceci est une décision de mise en œuvre et que le comportement par défaut pourrait changer à tout moment. Je me rends compte que je peux le modifier en utilisant [StructLayout] et [FieldOffset] , mais je préfère une solution qui ne le nécessite pas si possible.

Mon scénario de base est que j’ai une struct qui contient un champ de type référence et deux autres champs de type valeur, où ces champs sont de simples wrappers pour int . J’avais espéré que cela serait représenté par 16 octets sur le CLR 64 bits (8 pour la référence et 4 pour chacun des autres), mais pour une raison quelconque, il utilise 24 octets. Au fait, je mesure l’espace en utilisant des tableaux – je comprends que la mise en page peut être différente dans différentes situations, mais cela semblait être un sharepoint départ raisonnable.

Voici un exemple de programme démontrant le problème:

 using System; using System.Runtime.InteropServices; #pragma warning disable 0169 struct Int32Wrapper { int x; } struct TwoInt32s { int x, y; } struct TwoInt32Wrappers { Int32Wrapper x, y; } struct RefAndTwoInt32s { ssortingng text; int x, y; } struct RefAndTwoInt32Wrappers { ssortingng text; Int32Wrapper x, y; } class Test { static void Main() { Console.WriteLine("Environment: CLR {0} on {1} ({2})", Environment.Version, Environment.OSVersion, Environment.Is64BitProcess ? "64 bit" : "32 bit"); ShowSize(); ShowSize(); ShowSize(); ShowSize(); ShowSize(); } static void ShowSize() { long before = GC.GetTotalMemory(true); T[] array = new T[100000]; long after = GC.GetTotalMemory(true); Console.WriteLine("{0}: {1}", typeof(T), (after - before) / array.Length); } } 

Et la compilation et la sortie sur mon ordinateur portable:

 c:\Users\Jon\Test>csc /debug- /o+ ShowMemory.cs Microsoft (R) Visual C# Comstackr version 12.0.30501.0 for C# 5 Copyright (C) Microsoft Corporation. All rights reserved. c:\Users\Jon\Test>ShowMemory.exe Environment: CLR 4.0.30319.34014 on Microsoft Windows NT 6.2.9200.0 (64 bit) Int32Wrapper: 4 TwoInt32s: 8 TwoInt32Wrappers: 8 RefAndTwoInt32s: 16 RefAndTwoInt32Wrappers: 24 

Alors:

  • Si vous ne disposez pas d’un champ de type référence, le CLR est heureux de Int32Wrapper champs TwoInt32Wrappers ( TwoInt32Wrappers a une taille de 8)
  • Même avec un champ de type référence, le CLR est toujours content de regrouper des champs int ( RefAndTwoInt32s a une taille de 16)
  • En combinant les deux, chaque champ Int32Wrapper semble être complété / aligné sur 8 octets. ( RefAndTwoInt32Wrappers a une taille de 24.)
  • L’exécution du même code dans le débogueur (mais toujours une version de publication) affiche une taille de 12.

Quelques autres expériences ont donné des résultats similaires:

  • Le fait de placer le champ de type de référence après les champs de type valeur n’aide pas
  • Utiliser object au lieu de ssortingng n’aide pas (je suppose que c’est “tout type de référence”)
  • Utiliser une autre structure comme un “wrapper” autour de la référence n’aide pas
  • Utiliser une structure générique comme wrapper autour de la référence n’aide pas
  • Si je continue à append des champs (par paires pour plus de simplicité), les champs int comptent toujours pour 4 octets et les champs Int32Wrapper comptent pour 8 octets
  • Ajouter [StructLayout(LayoutKind.Sequential, Pack = 4)] à chaque structure en vue ne change pas les résultats

Quelqu’un at-il une explication à cela (idéalement avec de la documentation de référence) ou une suggestion sur la façon dont je peux obtenir des indices sur le CLR que je souhaite que les champs soient compressés sans spécifier un décalage de champ constant?

Je pense que c’est un bug. Vous voyez l’effet secondaire de la disposition automatique, il aime aligner des champs non sortingviaux sur une adresse multiple de 8 octets en mode 64 bits. Cela se produit même lorsque vous appliquez explicitement l’ [StructLayout(LayoutKind.Sequential)] . Ce n’est pas censé arriver.

Vous pouvez le voir en rendant publics les membres de la structure et en ajoutant un code de test comme celui-ci:

  var test = new RefAndTwoInt32Wrappers(); test.text = "adsf"; test.xx = 0x11111111; test.yx = 0x22222222; Console.ReadLine(); // <=== Breakpoint here 

Lorsque le point d'arrêt atteint, utilisez Debug + Windows + Memory + Memory 1. Basculez vers des entiers de 4 octets et mettez &test dans le champ Adresse:

  0x000000E928B5DE98 0ed750e0 000000e9 11111111 00000000 22222222 00000000 

0xe90ed750e0 est le pointeur de chaîne sur ma machine (pas le votre). Vous pouvez facilement voir les Int32Wrappers , avec les 4 octets supplémentaires de remplissage qui ont transformé la taille en 24 octets. Revenez à la structure et mettez la chaîne en dernier. Répétez et vous verrez que le pointeur de chaîne est toujours le premier. En LayoutKind.Sequential , vous avez LayoutKind.Auto .

Il sera difficile de convaincre Microsoft de résoudre ce problème, cela a fonctionné pendant trop longtemps, donc tout changement va casser quelque chose . Le CLR tente uniquement d'honorer [StructLayout] pour la version gérée d'une structure et de le rendre lisible, en général, il abandonne rapidement. Notoirement pour toute structure qui contient un DateTime. Vous obtenez uniquement la vraie garantie LayoutKind lors du marshaling d'une structure. La version Marshal.SizeOf() est certainement de 16 octets, comme Marshal.SizeOf() vous le dira.

Utiliser LayoutKind.Explicit répare, pas ce que vous vouliez entendre.

EDIT2

 struct RefAndTwoInt32Wrappers { public int x; public ssortingng s; } 

Ce code sera aligné sur 8 octets afin que la structure ait 16 octets. Par comparaison, ceci:

 struct RefAndTwoInt32Wrappers { public int x,y; public ssortingng s; } 

Sera aligné sur 4 octets donc cette structure aura également 16 octets. Donc, la justification ici est que la structure de l’algorithme CLR est déterminée par le nombre de champs les plus alignés, ce qui ne peut évidemment pas être fait par les classes, elles restront donc alignées sur 8 octets.

Maintenant, si nous combinons tout cela et créons une structure:

 struct RefAndTwoInt32Wrappers { public int x,y; public Int32Wrapper z; public ssortingng s; } 

Il aura 24 octets {x, y} auront chacun 4 octets et {z, s} aura 8 octets. Une fois que nous avons introduit un type de référence dans la structure, CLR alignera toujours notre structure personnalisée sur celle de la classe.

 struct RefAndTwoInt32Wrappers { public Int32Wrapper z; public long l; public int x,y; } 

Ce code aura 24 octets puisque Int32Wrapper sera aligné le même que long. Ainsi, le wrapper struct personnalisé sera toujours aligné sur le champ le plus haut / le mieux aligné de la structure ou sur ses propres champs internes les plus significatifs. Donc, dans le cas d’une chaîne de référence alignée sur 8 octets, le gestionnaire de structure s’alignera sur celle-ci.

Le champ de struct personnalisé final à l’intérieur de struct sera toujours aligné sur le champ d’instance aligné le plus élevé de la structure. Maintenant, si je ne suis pas sûr que ce soit un bug, mais sans preuve, je vais restr fidèle à mon opinion selon laquelle cela pourrait être une décision consciente.


MODIFIER

Les tailles ne sont en réalité précises que lorsqu’elles sont allouées sur un tas, mais les structures elles-mêmes ont des tailles plus petites (la taille exacte de ses champs). Une parsing plus poussée laisse à penser que cela pourrait être un bogue dans le code CLR, mais doit être étayé par des preuves.

Je vais inspecter le code cli et poster des mises à jour supplémentaires si quelque chose d’utile sera trouvé.


Ceci est une stratégie d’alignement utilisée par .NET mem allocator.

 public static RefAndTwoInt32s[] test = new RefAndTwoInt32s[1]; static void Main() { test[0].text = "a"; test[0].x = 1; test[0].x = 1; Console.ReadKey(); } 

Ce code compilé avec .net40 sous x64, dans WinDbg permet de faire ce qui suit:

Permet de trouver le type sur le tas d’abord:

  0:004> !dumpheap -type Ref Address MT Size 0000000003e72c78 000007fe61e8fb58 56 0000000003e72d08 000007fe039d3b78 40 Statistics: MT Count TotalSize Class Name 000007fe039d3b78 1 40 RefAndTwoInt32s[] 000007fe61e8fb58 1 56 System.Reflection.RuntimeAssembly Total 2 objects 

Une fois que nous l’avons, laisse voir ce qui se trouve sous cette adresse:

  0:004> !do 0000000003e72d08 Name: RefAndTwoInt32s[] MethodTable: 000007fe039d3b78 EEClass: 000007fe039d3ad0 Size: 40(0x28) bytes Array: Rank 1, Number of elements 1, Type VALUETYPE Fields: None 

Nous voyons que ceci est un ValueType et c’est celui que nous avons créé. Comme il s’agit d’un tableau, nous devons obtenir la valeur ValueType d’un seul élément dans le tableau:

  0:004> !dumparray -details 0000000003e72d08 Name: RefAndTwoInt32s[] MethodTable: 000007fe039d3b78 EEClass: 000007fe039d3ad0 Size: 40(0x28) bytes Array: Rank 1, Number of elements 1, Type VALUETYPE Element Methodtable: 000007fe039d3a58 [0] 0000000003e72d18 Name: RefAndTwoInt32s MethodTable: 000007fe039d3a58 EEClass: 000007fe03ae2338 Size: 32(0x20) bytes File: C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe Fields: MT Field Offset Type VT Attr Value Name 000007fe61e8c358 4000006 0 System.Ssortingng 0 instance 0000000003e72d30 text 000007fe61e8f108 4000007 8 System.Int32 1 instance 1 x 000007fe61e8f108 4000008 c System.Int32 1 instance 0 y 

La structure est en réalité de 32 octets car 16 octets sont réservés au remplissage, de sorte que chaque structure a une taille d’au moins 16 octets dès le départ.

si vous ajoutez 16 octets de ints et une chaîne de référence à: 0000000003e72d18 + 8 octets EE / padding, vous vous retrouvez à 0000000003e72d30 et c’est le sharepoint départ pour la référence de chaîne, et toutes les références étant complétées par 8 octets cela compense nos 32 octets pour cette structure.

Voyons si la chaîne est effectivement remplie de cette façon:

 0:004> !do 0000000003e72d30 Name: System.Ssortingng MethodTable: 000007fe61e8c358 EEClass: 000007fe617f3720 Size: 28(0x1c) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Ssortingng: a Fields: MT Field Offset Type VT Attr Value Name 000007fe61e8f108 40000aa 8 System.Int32 1 instance 1 m_ssortingngLength 000007fe61e8d640 40000ab c System.Char 1 instance 61 m_firstChar 000007fe61e8c358 40000ac 18 System.Ssortingng 0 shared static Empty >> Domain:Value 0000000001577e90:NotInit << 

Maintenant, analysons le programme ci-dessus de la même manière:

 public static RefAndTwoInt32Wrappers[] test = new RefAndTwoInt32Wrappers[1]; static void Main() { test[0].text = "a"; test[0].xx = 1; test[0].yx = 1; Console.ReadKey(); } 0:004> !dumpheap -type Ref Address MT Size 0000000003c22c78 000007fe61e8fb58 56 0000000003c22d08 000007fe039d3c00 48 Statistics: MT Count TotalSize Class Name 000007fe039d3c00 1 48 RefAndTwoInt32Wrappers[] 000007fe61e8fb58 1 56 System.Reflection.RuntimeAssembly Total 2 objects 

Notre struct est maintenant de 48 octets.

 0:004> !dumparray -details 0000000003c22d08 Name: RefAndTwoInt32Wrappers[] MethodTable: 000007fe039d3c00 EEClass: 000007fe039d3b58 Size: 48(0x30) bytes Array: Rank 1, Number of elements 1, Type VALUETYPE Element Methodtable: 000007fe039d3ae0 [0] 0000000003c22d18 Name: RefAndTwoInt32Wrappers MethodTable: 000007fe039d3ae0 EEClass: 000007fe03ae2338 Size: 40(0x28) bytes File: C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe Fields: MT Field Offset Type VT Attr Value Name 000007fe61e8c358 4000009 0 System.Ssortingng 0 instance 0000000003c22d38 text 000007fe039d3a20 400000a 8 Int32Wrapper 1 instance 0000000003c22d20 x 000007fe039d3a20 400000b 10 Int32Wrapper 1 instance 0000000003c22d28 y 

Ici, la situation est la même, si nous ajoutons à 0000000003c22d18 + 8 octets de chaîne ref, nous nous retrouverons au début du premier wrapper Int où la valeur pointe réellement vers l'adresse à laquelle nous sums.

Maintenant, nous pouvons voir que chaque valeur est une référence d'object, encore une fois, nous pouvons confirmer cela en regardant 0000000003c22d20.

 0:004> !do 0000000003c22d20  Invalid object 

En fait c'est correct puisque c'est une structure que l'adresse ne dit rien si c'est un obj ou vt.

 0:004> !dumpvc 000007fe039d3a20 0000000003c22d20 Name: Int32Wrapper MethodTable: 000007fe039d3a20 EEClass: 000007fe03ae23c8 Size: 24(0x18) bytes File: C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe Fields: MT Field Offset Type VT Attr Value Name 000007fe61e8f108 4000001 0 System.Int32 1 instance 1 x 

Donc, en réalité, cela ressemble plus à un type Union qui aura cette fois un alignement de 8 octets (tous les remplissages seront alignés avec la structure parente). Si ce n'était pas le cas, nous finirions avec 20 octets et ce n'est pas optimal pour que l'allocateur de mémoire ne le permette jamais. Si vous faites de nouveau le calcul, il apparaîtra que la structure est en effet de 40 octets de taille.

Donc, si vous voulez être plus prudent avec la mémoire, vous ne devriez jamais la mettre dans un type struct struct mais utiliser des tableaux simples. Une autre méthode consiste à allouer de la mémoire hors tas (VirtualAllocEx par exemple) de cette façon, vous recevez votre propre bloc de mémoire et vous le gérez comme vous le souhaitez.

La dernière question est de savoir pourquoi tout à coup, nous pourrions avoir une mise en page comme celle-là. Eh bien, si vous comparez le code jited et les performances d'une incrémentation int [] avec struct [] avec une incrémentation de champ de compteur, la seconde générera une adresse alignée de 8 octets, mais lorsqu'elle sera utilisée, cela se traduira par un code d'assemblage plus optimisé. LEA vs multiple MOV). Cependant, dans le cas décrit ici, les performances seront en fait pires, ce qui est cohérent avec l’implémentation CLR sous-jacente car il s’agit d’un type personnalisé pouvant comporter plusieurs champs, il est donc plus facile / préférable de valeur (puisque ce serait impossible) et faire un remplissage de structure là-bas, résultant ainsi en une taille d'octet plus grande.

Résumé voir la réponse de @Hans Passant probablement ci-dessus. La séquence séquentielle ne fonctionne pas


Quelques tests:

Ce n’est définitivement que sur 64 bits et la référence d’object “empoisonne” la structure. 32 bits fait ce que vous attendez:

 Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (32 bit) ConsoleApplication1.Int32Wrapper: 4 ConsoleApplication1.TwoInt32s: 8 ConsoleApplication1.TwoInt32Wrappers: 8 ConsoleApplication1.ThreeInt32Wrappers: 12 ConsoleApplication1.Ref: 4 ConsoleApplication1.RefAndTwoInt32s: 12 ConsoleApplication1.RefAndTwoInt32Wrappers: 12 ConsoleApplication1.RefAndThreeInt32s: 16 ConsoleApplication1.RefAndThreeInt32Wrappers: 16 

Dès que la référence d’object est ajoutée, toutes les structures s’étendent pour atteindre 8 octets plutôt que leur taille de 4 octets. Expansion des tests:

 Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (64 bit) ConsoleApplication1.Int32Wrapper: 4 ConsoleApplication1.TwoInt32s: 8 ConsoleApplication1.TwoInt32Wrappers: 8 ConsoleApplication1.ThreeInt32Wrappers: 12 ConsoleApplication1.Ref: 8 ConsoleApplication1.RefAndTwoInt32s: 16 ConsoleApplication1.RefAndTwoInt32sSequential: 16 ConsoleApplication1.RefAndTwoInt32Wrappers: 24 ConsoleApplication1.RefAndThreeInt32s: 24 ConsoleApplication1.RefAndThreeInt32Wrappers: 32 ConsoleApplication1.RefAndFourInt32s: 24 ConsoleApplication1.RefAndFourInt32Wrappers: 40 

Comme vous pouvez le constater, dès que la référence est ajoutée, chaque Int32Wrapper devient 8 octets, il n’est donc pas simple d’aligner. J’ai réduit l’allocation de tableau, dans la mesure où il s’agissait d’une allocation LoH qui est alignée différemment.

Juste pour append des données au mix – j’ai créé un type de plus que ceux que vous aviez:

 struct RefAndTwoInt32Wrappers2 { ssortingng text; TwoInt32Wrappers z; } 

Le programme écrit:

 RefAndTwoInt32Wrappers2: 16 

Il semble donc que la structure TwoInt32Wrappers s’aligne correctement dans la nouvelle structure RefAndTwoInt32Wrappers2 .