120 votes

Pourquoi l'alignement des structures dépend-il du fait qu'un type de champ est primitif ou défini par l'utilisateur ?

Sur Heure de Noda v2, nous passons à une résolution de l'ordre de la nanoseconde. Cela signifie que nous ne pouvons plus utiliser un entier de 8 octets pour représenter toute la gamme de temps qui nous intéresse. Cela m'a incité à enquêter sur l'utilisation de la mémoire des (nombreuses) structures de Noda Time, ce qui m'a conduit à découvrir une légère bizarrerie dans la décision d'alignement du CLR.

Tout d'abord, je réalise que cette est une décision de mise en œuvre, et que le comportement par défaut pourrait changer à tout moment. Je réalise que je peut le modifier en utilisant [StructLayout] et [FieldOffset] mais je préférerais, si possible, trouver une solution qui n'exige pas cela.

Mon scénario de base est le suivant : 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 enveloppes 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 en utilisant des tableaux, d'ailleurs - je comprends que la disposition peut être différente dans des situations différentes, mais cela m'a semblé ê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 :

  • Si vous n'avez pas de champ de type référence, le CLR se contente de packager Int32Wrapper les champs ensemble ( TwoInt32Wrappers a une taille de 8)
  • Même avec un champ de type référence, le CLR est toujours heureux d'emballer int les champs ensemble ( RefAndTwoInt32s a une taille de 16)
  • En combinant les deux, chaque Int32Wrapper 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 il s'agit toujours d'une version publiée) montre une taille de 12.

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

  • Mettre le champ de type référence après les champs de type valeur n'aide pas.
  • Utilisation de object au lieu de string n'aide pas (je pense que c'est "tout type de référence").
  • L'utilisation d'une autre structure comme "enveloppe" autour de la référence n'est pas utile.
  • L'utilisation d'une structure générique comme enveloppe autour de la référence n'aide pas.
  • Si je continue à ajouter des champs (par paires pour plus de simplicité), int comptent toujours pour 4 octets, et Int32Wrapper nombre de champs pour 8 octets
  • Ajout de [StructLayout(LayoutKind.Sequential, Pack = 4)] à chaque structure en vue ne change pas les résultats.

Quelqu'un a-t-il une explication à ce problème (idéalement avec une documentation de référence) ou une suggestion sur la façon dont je peux indiquer au CLR que je souhaite que les champs soient emballés ? sans en spécifiant un décalage de champ constant ?

85voto

Hans Passant Points 475940

Je pense que c'est un bug. Vous voyez l'effet secondaire de la mise en page automatique, qui 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'option [StructLayout(LayoutKind.Sequential)] attribut. Cela 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 est atteint, utilisez Debug + Windows + Memory + Memory 1. Passez à 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. Retournez à la structure et placez la chaîne en dernier. Répétez et vous verrez que le pointeur de la chaîne est toujours première. Violation de LayoutKind.Sequential vous avez LayoutKind.Auto .

Il sera difficile de convaincre Microsoft de corriger ce problème, car le système fonctionne de cette manière depuis trop longtemps et tout changement sera une rupture. quelque chose . Le CLR ne fait qu'essayer d'honorer [StructLayout] pour la version gérée d'une structure et la rendre blittable, elle abandonne en général rapidement. Notamment pour toute structure qui contient un DateTime. Vous n'obtenez la vraie garantie de LayoutKind que lorsque vous marshalez une structure. La version marshaled est certainement de 16 octets, comme Marshal.SizeOf() vous le dira.

Utilisation de LayoutKind.Explicit le fixe, pas ce que tu voulais entendre.

19voto

EDIT2

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

Ce code sera aligné sur 8 octets et la structure aura donc 16 octets. En comparaison, ceci :

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

sera aligné sur 4 octets, donc cette structure aura aussi 16 octets. Le raisonnement ici est que l'alignement des structures dans le CLR est déterminé par le nombre de champs les plus alignés, les clases ne peuvent évidemment pas faire cela, elles resteront donc alignées sur 8 octets.

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

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

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

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

Ce code aura 24 octets puisque Int32Wrapper sera aligné de la même manière que long. Ainsi, le wrapper de structure personnalisé s'alignera toujours sur le champ le plus élevé/le mieux aligné de la structure ou sur ses propres champs internes les plus significatifs. Ainsi, dans le cas d'une chaîne de ref qui est alignée sur 8 octets, le wrapper struct s'alignera sur ce champ.

La conclusion d'un champ de structure personnalisé à l'intérieur d'une structure sera toujours alignée sur le champ d'instance le plus aligné de la structure. Maintenant, je ne suis pas sûr qu'il s'agisse d'un bug, mais sans preuve, je vais m'en tenir à mon opinion selon laquelle il pourrait s'agir d'une décision consciente.


EDIT

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 leurs champs). Une analyse plus poussée semble suggérer qu'il pourrait s'agir d'un bogue dans le code du CLR, mais elle doit être étayée par des preuves.

Je vais inspecter le code de la falaise et poster d'autres mises à jour si quelque chose d'utile est trouvé.


Il s'agit d'une stratégie d'alignement utilisée par l'allocateur de mémoire .NET.

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 a été compilé avec .net40 sous x64, Dans WinDbg faisons ce qui suit :

Trouvons d'abord le type sur le tas :

    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'aurons, voyons ce qu'il y a 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 le ValueType def 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 puisque ses 16 octets sont réservés pour le remplissage. En réalité, chaque structure a une taille d'au moins 16 octets dès le départ.

si vous ajoutez 16 octets d'ints et une chaîne de référence à : 0000000003e72d18 + 8 octets EE/padding, on obtient 0000000003e72d30. C'est le point de départ de la référence de la chaîne de caractères, et comme toutes les références sont paddées de 8 octets à partir de leur premier champ de données réel, cela nous permet de disposer de 32 octets pour cette structure.

Voyons si la chaîne de caractères est vraiment 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  <<

Analysons maintenant 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 structure 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 référence de chaîne, nous nous retrouverons au début du premier wrapper Int où la valeur pointe réellement vers l'adresse où nous nous trouvons.

Maintenant nous pouvons voir que chaque valeur est une référence d'objet, confirmons-le en regardant 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 pas si c'est un obj ou 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

En réalité, il s'agit plutôt d'un type d'Union qui sera aligné sur 8 octets cette fois-ci (tous les espaces seront alignés sur la structure parente). Si ce n'était pas le cas, nous nous retrouverions avec 20 octets, ce qui n'est pas optimal et l'allocateur de mémoire ne le permettra jamais. Si vous refaites le calcul, vous verrez que la structure a effectivement une taille de 40 octets.

Donc, si vous voulez être plus conservateur avec la mémoire, vous ne devriez jamais l'emballer dans un type struct personnalisé, mais plutôt utiliser des tableaux simples. Une autre méthode consiste à allouer de la mémoire hors du tas (VirtualAllocEx, par exemple). de cette façon, vous disposez de votre propre bloc de mémoire et vous pouvez le gérer comme vous le souhaitez.

La dernière question est de savoir pourquoi, tout d'un coup, nous pouvons obtenir une telle mise en page. 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 lorsqu'il est jited, cela se traduit par un code assembleur plus optimisé (un seul LEA contre plusieurs MOV). Cependant, dans le cas décrit ici, les performances seront en fait plus mauvaises. Je pense donc que c'est cohérent avec l'implémentation sous-jacente du CLR puisqu'il s'agit d'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'une valeur (puisque ce serait impossible) et de faire un remplissage de structure à cet endroit, ce qui donne une taille d'octet plus grande.

9voto

BenAdams Points 605

Résumé voir la réponse de @Hans Passant probablement ci-dessus. Layout Sequential ne fonctionne pas


Quelques tests :

Ce n'est certainement que sur 64 bits et la référence à l'objet "empoisonne" la structure. Le 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 de l'objet est ajoutée, toutes les structures sont étendues à 8 octets au lieu de leur taille de 4 octets. Agrandissement 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 qui ne constitue pas un simple alignement. J'ai réduit l'allocation du tableau au cas où il s'agirait d'une allocation LoH qui est alignée différemment.

4voto

Jesse C. Slicer Points 11750

Juste pour ajouter quelques données au mélange - j'ai créé un type supplémentaire à partir de ceux que vous aviez :

struct RefAndTwoInt32Wrappers2
{
    string text;
    TwoInt32Wrappers z;
}

Le programme écrit :

RefAndTwoInt32Wrappers2: 16

Il semble donc que le TwoInt32Wrappers s'aligne correctement dans la nouvelle RefAndTwoInt32Wrappers2 struct.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X