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.
Tout d'abord, je me rends compte que ceci est une décision d'implémentation, 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é comme 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. Soit dit en passant, je mesure l'espace à l'aide de tableaux - je comprends que la disposition peut être différente dans différentes situations, mais cela 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
Donc:
Int32Wrapper
(TwoInt32Wrappers
A une taille de 8)int
champs ensemble (RefAndTwoInt32s
A une taille de 16)Int32Wrapper
Semble être rempli/aligné sur 8 octets. (RefAndTwoInt32Wrappers
A une taille de 24.)Quelques autres expériences ont donné des résultats similaires:
object
au lieu de string
n'aide pas (je suppose que c'est "n'importe quel type de référence")int
comptent toujours pour 4 octets et les champs Int32Wrapper
Comptent pour 8 octets[StructLayout(LayoutKind.Sequential, Pack = 4)]
à chaque structure en vue ne change pas les résultatsQuelqu'un at-il une explication à cela (idéalement avec une documentation de référence) ou une suggestion sur la façon dont je peux obtenir un indice pour le CLR que je voudrais que les champs soient compressés sans spécifiant un décalage de champ constant?
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 qui est un multiple de 8 octets en mode 64 bits. Cela se produit même lorsque vous appliquez explicitement l'attribut [StructLayout(LayoutKind.Sequential)]
. Ce n'est pas censé se produire.
Vous pouvez le voir en rendant les membres de la structure publics et en ajoutant du code de test comme ceci:
var test = new RefAndTwoInt32Wrappers();
test.text = "adsf";
test.x.x = 0x11111111;
test.y.x = 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 la vôtre). Vous pouvez facilement voir le 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 la chaîne est toujours en premier. En violant LayoutKind.Sequential
, Vous obtenez LayoutKind.Auto
.
Il va être difficile de convaincre Microsoft de résoudre ce problème, cela a fonctionné trop longtemps de cette façon, donc tout changement va se casser quelque chose. Le CLR ne tente d'honorer [StructLayout]
Que pour la version gérée d'une structure et de la rendre blittable, elle abandonne en général rapidement. Notoirement pour toute structure contenant un DateTime. Vous n'obtenez la vraie garantie LayoutKind que lors du marshaling d'une structure. La version marshalée fait certainement 16 octets, comme vous le dira Marshal.SizeOf()
.
L'utilisation de LayoutKind.Explicit
Le corrige, pas ce que vous vouliez entendre.
EDIT2
struct RefAndTwoInt32Wrappers
{
public int x;
public string s;
}
Ce code sera aligné sur 8 octets donc la structure aura 16 octets. En comparaison, ceci:
struct RefAndTwoInt32Wrappers
{
public int x,y;
public string s;
}
Sera aligné sur 4 octets donc cette structure aura également 16 octets. Donc, la raison ici est que l'alignement de la structure dans CLR est déterminé par le nombre de champs les plus alignés, les classes ne peuvent évidemment pas le faire, donc elles resteront 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 string s;
}
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.
struct RefAndTwoInt32Wrappers
{
public Int32Wrapper z;
public long l;
public int x,y;
}
Ce code aura 24 octets car Int32Wrapper sera aligné de la même manière aussi longtemps. Ainsi, l'encapsuleur de structure personnalisé sera toujours aligné sur le champ le plus élevé/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 wrapper struct s'alignera sur cela.
La conclusion du champ de structure personnalisé à l'intérieur de la structure sera toujours alignée 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 m'en tenir à mon avis que cela pourrait être une décision consciente.
MODIFIER
Les tailles ne sont en fait précises que 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). Analyse approfondie pour suggérer que cela pourrait être un bogue dans le code CLR, mais doit être étayé par des preuves.
Je vais inspecter le code cli et publier 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 .NET mem.
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 en premier:
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, voyons 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 c'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 du 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.String 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 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 d'ints et une référence de chaîne à: 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 effectivement remplie de cette façon:
0:004> !do 0000000003e72d30
Name: System.String
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
String: a
Fields:
MT Field Offset Type VT Attr Value Name
000007fe61e8f108 40000aa 8 System.Int32 1 instance 1 m_stringLength
000007fe61e8d640 40000ab c System.Char 1 instance 61 m_firstChar
000007fe61e8c358 40000ac 18 System.String 0 shared static Empty
>> Domain:Value 0000000001577e90:NotInit <<
Permet maintenant d'analyser 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].x.x = 1;
test[0].y.x = 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.String 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 où nous sommes.
Maintenant, nous pouvons voir que chaque valeur est une référence d'objet permet à nouveau de le confirmer en jetant un coup d'œil à 0000000003c22d20.
0:004> !do 0000000003c22d20
<Note: this object has an invalid CLASS field>
Invalid object
En fait, c'est correct puisque c'est une structure, l'adresse ne nous dit rien s'il s'agit d'un obj ou d'un 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 obtiendra cette fois 8 octets alignés (tous les remplissages seront alignés avec la structure parent). Si ce n'était pas le cas, nous finirions avec 20 octets et ce n'est pas optimal, donc l'allocateur mem ne permettra jamais que cela se produise. Si vous refaites le calcul, il se trouvera que la structure fait en effet 40 octets de taille.
Donc, si vous voulez être plus conservateur avec la mémoire, vous ne devriez jamais l'emballer dans un type struct struct struct mais utiliser plutôt des tableaux simples. Une autre façon consiste à allouer de la mémoire hors du tas (VirtualAllocEx par exemple) de cette façon, on vous donne votre propre bloc de mémoire et vous le gérez comme vous le souhaitez.
La dernière question ici est pourquoi tout à coup nous pourrions obtenir une mise en page comme ça. 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 sur 8 octets étant une union, mais lorsqu'elle est jited, cela se traduit par un code d'assemblage plus optimisé (singe LEA vs plusieurs MOV). Cependant, dans le cas décrit ici, les performances seront en fait moins bonnes, donc je pense que cela est cohérent avec l'implémentation CLR sous-jacente, car il s'agit d'un type personnalisé qui peut avoir plusieurs champs, il peut donc être plus facile/préférable de mettre l'adresse de départ au lieu d'un valeur (car ce serait impossible) et y faire un remplissage de struct, ce qui entraîne une taille d'octet plus grande.
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 définitivement que sur 64 bits et la référence d'objet "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'objet est ajoutée, toutes les structures se développent pour être de 8 octets plutôt que leur taille de 4 octets. Extension 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 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 de tableau au cas où c'était l'allocation LoH qui est différemment alignée.
Juste pour ajouter quelques données au mélange - j'ai créé un type de plus à partir de ceux que vous aviez:
struct RefAndTwoInt32Wrappers2
{
string text;
TwoInt32Wrappers z;
}
Le programme écrit:
RefAndTwoInt32Wrappers2: 16
Il ressemble donc au TwoInt32Wrappers
struct s'aligne correctement dans le nouveau RefAndTwoInt32Wrappers2
struct.