Dans Noda Time v2, nous passons à une résolution nanoseconde. 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.
Tout d'abord, je me rends compte qu'il s'agit d' une décision de mise en œuvre et que le comportement par défaut peut changer à tout moment. Je me rends compte que je peux le modifier en utilisant [StructLayout]
et [FieldOffset]
, mais je préfère trouver une solution qui ne l'exige pas si possible.
Mon scénario principal est que j'ai un 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. Je mesure l'espace à l'aide de tableaux, au fait - je comprends que la disposition peut être différente dans différentes situations, mais cela me semblait être un point de départ raisonnable.
Voici un exemple de programme illustrant 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
{
string text;
int x, y;
}
struct RefAndTwoInt32Wrappers
{
string 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<Int32Wrapper>();
ShowSize<TwoInt32s>();
ShowSize<TwoInt32Wrappers>();
ShowSize<RefAndTwoInt32s>();
ShowSize<RefAndTwoInt32Wrappers>();
}
static void ShowSize<T>()
{
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# Compiler 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 n'avez pas de champ de type de référence, le CLR est heureux de
Int32Wrapper
regrouper les champs (TwoInt32Wrappers
a une taille de 8) - Même avec un champ de type de référence, le CLR est toujours heureux d'emballer
int
regrouper les champs (RefAndTwoInt32s
a une taille de 16) - En combinant les deux, chaque
Int32Wrapper
champ semble être rempli / 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 version) affiche une taille de 12.
Quelques autres expériences ont donné des résultats similaires:
- Mettre le champ de type de référence après les champs de type de valeur n'aide pas
- Utiliser
object
au lieu destring
n'aide pas (je suppose que c'est "n'importe quel type de référence") - Utiliser une autre structure comme "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 d'ajouter des champs (par paires pour plus de simplicité), les
int
champs comptent toujours pour 4 octets, etInt32Wrapper
champs comptent pour 8 octets - L'ajout
[StructLayout(LayoutKind.Sequential, Pack = 4)]
à chaque structure en vue ne change pas les résultats
Quelqu'un a-t-il une explication à ce sujet (idéalement avec une documentation de référence) ou une suggestion sur la façon dont je peux obtenir un indice au CLR que je voudrais que les champs soient compilés sans spécifier un décalage de champ constant?
Ref<T>
mais utilisez à lastring
place, non pas que cela devrait faire une différence.TwoInt32Wrappers
, ou unInt64
et unTwoInt32Wrappers
? Et si vous créez un génériquePair<T1,T2> {public T1 f1; public T2 f2;}
puis créezPair<string,Pair<int,int>>
etPair<string,Pair<Int32Wrapper,Int32Wrapper>>
? Quelles combinaisons obligent le JITter à rembourrer les choses?Pair<string, TwoInt32Wrappers>
ne donne que 16 octets, ce qui résoudrait le problème. Fascinant.Marshal.SizeOf
renverra la taille de la structure qui serait passée au code natif, qui ne doit avoir aucune relation avec la taille de la structure dans le code .NET.Réponses:
Je pense que c'est un bug. Vous voyez l'effet secondaire de la mise en page automatique, il aime aligner les champs non triviaux sur une adresse multiple de 8 octets en mode 64 bits. Cela se produit même lorsque vous appliquez explicitement le
[StructLayout(LayoutKind.Sequential)]
attribut. Ce n'est pas censé arriver.Vous pouvez le voir en rendant les membres de la structure publics et en ajoutant du code de test comme ceci:
Lorsque le point d'arrêt atteint, utilisez Debug + Windows + Memory + Memory 1. Passez à des entiers de 4 octets et mettez
&test
dans le champ Adresse:0xe90ed750e0
est le pointeur de chaîne sur ma machine (pas la vôtre). Vous pouvez facilement voir leInt32Wrappers
, 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. ViolerLayoutKind.Sequential
, vous avezLayoutKind.Auto
.Il sera difficile de convaincre Microsoft de résoudre ce problème, cela fonctionne de cette façon depuis trop longtemps, donc tout changement va être rompu quelque chose . Le CLR ne fait qu'une tentative d'honorer
[StructLayout]
la version gérée d'une structure et de la rendre blittable, il abandonne rapidement en général. Notoirement pour toute structure qui contient un DateTime. Vous n'obtenez la véritable garantie LayoutKind que lors du marshaling d'une structure. La version marshalée est certainement de 16 octets, commeMarshal.SizeOf()
vous le direz.Utiliser le
LayoutKind.Explicit
corrige, pas ce que vous vouliez entendre.la source
string
par un autre nouveau type de référence (class
) auquel on s'est appliqué[StructLayout(LayoutKind.Sequential)]
ne semble rien changer. Dans la direction opposée, en appliquant[StructLayout(LayoutKind.Auto)]
auxstruct Int32Wrapper
changements l'utilisation de la mémoire dansTwoInt32Wrappers
.MODIFIER2
Ce code sera aligné sur 8 octets, donc la structure aura 16 octets. Par comparaison ceci:
Sera aligné sur 4 octets, donc cette structure aura également 16 octets. Donc, la logique ici est que l'alignement de structure dans CLR est déterminé par le nombre de champs les plus alignés, les classes ne peuvent évidemment pas le faire, elles resteront donc alignées sur 8 octets.
Maintenant, si nous combinons tout cela et créons une structure:
Il aura 24 octets {x, y} aura 4 octets chacun et {z, s} aura 8 octets. Une fois que nous introduisons un type ref dans la structure, CLR alignera toujours notre structure personnalisée pour correspondre à l'alignement de classe.
Ce code aura 24 octets puisque Int32Wrapper sera aligné de la même manière que long. Ainsi, l'encapsuleur de structure personnalisé s'alignera toujours 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, l'encapsuleur de structure s'alignera sur cela.
Le champ struct personnalisé final à l'intérieur de struct sera toujours aligné sur le champ d'instance aligné le plus haut de la structure. Maintenant, si je ne suis pas sûr qu'il s'agisse d'un bug, mais sans aucune preuve, je vais m'en tenir à mon opinion que cela pourrait être une décision consciente.
ÉDITER
Les tailles sont en fait précises uniquement lorsqu'elles sont allouées sur un tas, mais les structures elles-mêmes ont des tailles plus petites (les tailles exactes de ses champs). Une analyse plus approfondie suggère que cela pourrait être un bogue dans le code CLR, mais qu'il doit être soutenu par des preuves.
J'inspecterai le code cli et publierai d'autres mises à jour si quelque chose d'utile sera trouvé.
Il s'agit d'une stratégie d'alignement utilisée par l'allocateur de mémoire .NET.
Ce code compilé avec .net40 sous x64, dans WinDbg permet de faire ce qui suit:
Permet de trouver d'abord le type sur le tas:
Une fois que nous l'avons, voyons ce qu'il y a sous cette adresse:
Nous voyons que c'est un ValueType et c'est celui que nous avons créé. Puisqu'il s'agit d'un tableau, nous devons obtenir le ValueType def d'un seul élément du tableau:
La structure est en fait de 32 octets car ses 16 octets sont réservés pour le remplissage, donc en réalité, chaque structure a une taille d'au moins 16 octets dès le départ.
si vous ajoutez 16 octets à partir des entiers et une chaîne de référence à: 0000000003e72d18 + 8 octets EE / padding, vous vous retrouverez à 0000000003e72d30 et c'est le point de départ pour la référence de chaîne, et puisque toutes les références sont remplies de 8 octets à partir de leur premier champ de données réel cela compense nos 32 octets pour cette structure.
Voyons si la chaîne est réellement remplie de cette façon:
Analysons maintenant le programme ci-dessus de la même manière:
Notre structure est maintenant de 48 octets.
Ici, la situation est la même, si nous ajoutons à 0000000003c22d18 + 8 octets de string ref, nous nous retrouverons au début du premier wrapper Int où la valeur pointe réellement vers l'adresse à laquelle nous nous trouvons.
Maintenant, nous pouvons voir que chaque valeur est une référence d'objet à nouveau, confirmons cela en jetant un œil à 0000000003c22d20.
En fait, c'est correct car c'est une structure, l'adresse ne nous dit rien s'il s'agit d'un obj ou d'un vt.
Donc, en réalité, cela ressemble plus à un type Union qui obtiendra 8 octets alignés cette fois-ci (tous les bourrages seront alignés avec la structure parent). Si ce n'était pas le cas, nous nous retrouverions avec 20 octets et ce n'est pas optimal, donc l'allocateur de mémoire ne permettra jamais que cela se produise. Si vous refaites le calcul, il s'avérera que la structure a en effet une taille de 40 octets.
Donc, si vous voulez être plus conservateur avec la mémoire, vous ne devez jamais la mettre dans un type struct personnalisé struct mais utiliser à la place des tableaux simples. Une autre façon est d'allouer de la mémoire hors tas (VirtualAllocEx par exemple) de cette façon, vous disposez de votre propre bloc de mémoire et vous le gérez comme vous le souhaitez.
La dernière question ici est de savoir pourquoi tout à coup nous pourrions obtenir 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, le second générera une adresse alignée de 8 octets étant une union, mais lorsque jited cela se traduit par un code d'assemblage plus optimisé (singe LEA vs MOV multiple). Cependant, dans le cas décrit ici, les performances seront en fait pires, donc je pense que cela est cohérent avec l'implémentation CLR sous-jacente car c'est un type personnalisé qui peut avoir plusieurs champs, il peut donc être plus facile / meilleur de mettre l'adresse de départ au lieu d'un value (car ce serait impossible) et effectuez un remplissage de structure à cet endroit, ce qui entraîne une taille d'octet plus grande.
la source
RefAndTwoInt32Wrappers
n'est pas de 32 octets - c'est 24, ce qui est le même que celui indiqué avec mon code. Si vous regardez dans la vue de la mémoire au lieu d'utiliserdumparray
, et regardez la mémoire pour un tableau avec (disons) 3 éléments avec des valeurs distinctes, vous pouvez clairement voir que chaque élément se compose d'une référence de chaîne de 8 octets et de deux entiers de 8 octets . Je soupçonne que celadumparray
montre les valeurs comme références simplement parce qu'il ne sait pas comment afficher lesInt32Wrapper
valeurs. Ces «références» se désignent elles-mêmes; ce ne sont pas des valeurs distinctes.dumparray
indiqué.Int32
. Je ne suis pas trop dérangé par ce qu'il fait sur la pile, pour être honnête - mais je ne l'ai pas vérifié.Résumé voir la réponse de @Hans Passant probablement ci-dessus. La mise en page séquentielle ne fonctionne pas
Quelques tests:
Ce n'est certainement que sur 64 bits et la référence d'objet "empoisonne" la structure. 32 bits fait ce que vous attendez:
Dès que la référence d'objet est ajoutée, toutes les structures se développent pour être de 8 octets plutôt que leur taille de 4 octets. Élargir les tests:
Comme vous pouvez le voir dès que la référence est ajoutée, chaque Int32Wrapper devient 8 octets, ce n'est donc pas un simple alignement. J'ai réduit l'allocation du tableau au cas où il s'agissait d'une allocation LoH qui est alignée différemment.
la source
Juste pour ajouter des données au mix - j'ai créé un autre type à partir de ceux que vous aviez:
Le programme écrit:
Donc il ressemble au
TwoInt32Wrappers
structure s'aligne correctement dans la nouvelleRefAndTwoInt32Wrappers2
structure.la source