Est-ce que l'utilisation de «nouveau» sur une structure l'alloue sur le tas ou la pile?

290

Lorsque vous créez une instance d'une classe avec l' newopérateur, la mémoire est allouée sur le tas. Lorsque vous créez une instance d'une structure avec l' newopérateur où la mémoire est-elle allouée, sur le tas ou sur la pile?

kedar kamthe
la source

Réponses:

305

D'accord, voyons si je peux rendre cela plus clair.

Tout d'abord, Ash a raison: la question n'est pas de savoir où les variables de type valeur sont allouées. C'est une question différente - et à laquelle la réponse n'est pas seulement "sur la pile". C'est plus compliqué que ça (et rendu encore plus compliqué par C # 2). J'ai un article sur le sujet et je le développerai si demandé, mais traitons simplementnew opérateur.

Deuxièmement, tout cela dépend vraiment du niveau dont vous parlez. Je regarde ce que le compilateur fait avec le code source, en termes d'IL qu'il crée. Il est plus que possible que le compilateur JIT fasse des choses intelligentes en termes d'optimisation de beaucoup d'allocation "logique".

Troisièmement, j'ignore les génériques, principalement parce que je ne connais pas vraiment la réponse, et en partie parce que cela compliquerait trop les choses.

Enfin, tout cela ne concerne que l'implémentation actuelle. La spécification C # ne spécifie pas grand-chose - c'est en fait un détail d'implémentation. Il y a ceux qui pensent que les développeurs de code managé ne devraient vraiment pas s'en soucier. Je ne suis pas sûr que j'irais aussi loin, mais cela vaut la peine d'imaginer un monde où, en fait, toutes les variables locales vivent sur le tas - ce qui serait toujours conforme à la spécification.


Il existe deux situations différentes avec l' newopérateur sur les types de valeur: vous pouvez appeler un constructeur sans paramètre (par exemple new Guid()) ou un constructeur avec paramètre (par exemple new Guid(someString)). Ceux-ci génèrent une IL significativement différente. Pour comprendre pourquoi, vous devez comparer les spécifications C # et CLI: selon C #, tous les types de valeur ont un constructeur sans paramètre. Selon la spécification CLI, aucun type de valeur n'a de constructeur sans paramètre. (Récupérez les constructeurs d'un type de valeur avec réflexion un certain temps - vous n'en trouverez pas un sans paramètre.)

Il est logique que C # traite le "initialiser une valeur avec des zéros" comme un constructeur, car il maintient la cohérence du langage - vous pouvez penser new(...)comme toujours appeler un constructeur. Il est logique que la CLI pense différemment, car il n'y a pas de vrai code à appeler - et certainement pas de code spécifique au type.

Cela fait également une différence ce que vous allez faire avec la valeur après l'avoir initialisée. L'IL utilisé pour

Guid localVariable = new Guid(someString);

est différent de l'IL utilisé pour:

myInstanceOrStaticVariable = new Guid(someString);

De plus, si la valeur est utilisée comme valeur intermédiaire, par exemple un argument pour un appel de méthode, les choses sont à nouveau légèrement différentes. Pour montrer toutes ces différences, voici un petit programme de test. Il ne montre pas la différence entre les variables statiques et les variables d'instance: l'IL serait différent entre stfldet stsfld, mais c'est tout.

using System;

public class Test
{
    static Guid field;

    static void Main() {}
    static void MethodTakingGuid(Guid guid) {}


    static void ParameterisedCtorAssignToField()
    {
        field = new Guid("");
    }

    static void ParameterisedCtorAssignToLocal()
    {
        Guid local = new Guid("");
        // Force the value to be used
        local.ToString();
    }

    static void ParameterisedCtorCallMethod()
    {
        MethodTakingGuid(new Guid(""));
    }

    static void ParameterlessCtorAssignToField()
    {
        field = new Guid();
    }

    static void ParameterlessCtorAssignToLocal()
    {
        Guid local = new Guid();
        // Force the value to be used
        local.ToString();
    }

    static void ParameterlessCtorCallMethod()
    {
        MethodTakingGuid(new Guid());
    }
}

Voici l'IL de la classe, à l'exclusion des bits non pertinents (tels que nops):

.class public auto ansi beforefieldinit Test extends [mscorlib]System.Object    
{
    // Removed Test's constructor, Main, and MethodTakingGuid.

    .method private hidebysig static void ParameterisedCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: stsfld valuetype [mscorlib]System.Guid Test::field
        L_0010: ret     
    }

    .method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed
    {
        .maxstack 2
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid    
        L_0003: ldstr ""    
        L_0008: call instance void [mscorlib]System.Guid::.ctor(string)    
        // Removed ToString() call
        L_001c: ret
    }

    .method private hidebysig static void ParameterisedCtorCallMethod() cil  managed    
    {   
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0011: ret     
    }

    .method private hidebysig static void ParameterlessCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field
        L_0006: initobj [mscorlib]System.Guid
        L_000c: ret 
    }

    .method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        // Removed ToString() call
        L_0017: ret 
    }

    .method private hidebysig static void ParameterlessCtorCallMethod() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        L_0009: ldloc.0 
        L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0010: ret 
    }

    .field private static valuetype [mscorlib]System.Guid field
}

Comme vous pouvez le voir, de nombreuses instructions différentes sont utilisées pour appeler le constructeur:

  • newobj: Alloue la valeur sur la pile, appelle un constructeur paramétré. Utilisé pour les valeurs intermédiaires, par exemple pour l'affectation à un champ ou comme argument de méthode.
  • call instance: Utilise un emplacement de stockage déjà alloué (sur la pile ou non). Ceci est utilisé dans le code ci-dessus pour l'affectation à une variable locale. Si la même variable locale se voit attribuer plusieurs fois une valeur à l'aide de plusieurs newappels, elle initialise simplement les données au-dessus de l'ancienne valeur - elle n'alloue pas plus d'espace de pile à chaque fois.
  • initobj: Utilise un emplacement de stockage déjà alloué et efface simplement les données. Ceci est utilisé pour tous nos appels de constructeur sans paramètre, y compris ceux qui sont affectés à une variable locale. Pour l'appel de méthode, une variable locale intermédiaire est effectivement introduite et sa valeur est effacée par initobj.

J'espère que cela montre à quel point le sujet est compliqué, tout en mettant un peu de lumière dessus en même temps. Dans certains sens conceptuels, chaque appel à newallouer de l'espace sur la pile - mais comme nous l'avons vu, ce n'est pas vraiment ce qui se passe même au niveau IL. Je voudrais souligner un cas particulier. Prenez cette méthode:

void HowManyStackAllocations()
{
    Guid guid = new Guid();
    // [...] Use guid
    guid = new Guid(someBytes);
    // [...] Use guid
    guid = new Guid(someString);
    // [...] Use guid
}

Ce "logiquement" a 4 allocations de pile - une pour la variable et une pour chacun des trois newappels - mais en fait (pour ce code spécifique) la pile n'est allouée qu'une seule fois, puis le même emplacement de stockage est réutilisé.

EDIT: Juste pour être clair, cela n'est vrai que dans certains cas ... en particulier, la valeur de guidne sera pas visible si le Guidconstructeur lève une exception, c'est pourquoi le compilateur C # est capable de réutiliser le même emplacement de pile. Voir le blog d' Eric Lippert sur la construction de types de valeur pour plus de détails et un cas où cela ne s'applique pas .

J'ai beaucoup appris en écrivant cette réponse - veuillez demander des éclaircissements si l'un d'eux n'est pas clair!

Jon Skeet
la source
1
Jon, l'exemple de code HowManyStackAllocations est bon. Mais pouvez-vous le modifier pour utiliser un Struct au lieu de Guid, ou ajouter un nouvel exemple Struct. Je pense que cela répondrait alors directement à la question initiale de @ kedar.
Ash
9
Guid est déjà une structure. Voir msdn.microsoft.com/en-us/library/system.guid.aspx Je n'aurais pas choisi un type de référence pour cette question :)
Jon Skeet
1
Que se passe-t-il lorsque vous en avez List<Guid>et ajoutez-les? Ce serait 3 allocations (même IL)? Mais ils sont gardés dans un endroit magique
Arec Barrwin
1
@Ani: Vous manquez le fait que l'exemple d'Eric a un bloc try / catch - donc si une exception est levée pendant le constructeur de la structure, vous devez pouvoir voir la valeur avant le constructeur. Mon exemple n'a pas une telle situation - si le constructeur échoue avec une exception, peu importe si la valeur de guidn'a été qu'à moitié écrasée, car elle ne sera pas visible de toute façon.
Jon Skeet
2
@Ani: En fait, Eric appelle cela au bas de son message: "Et le point de Wesner? Oui, en fait, si c'est une variable locale allouée à la pile (et non un champ dans une fermeture) qui est déclarée au même niveau d'imbrication "try" que l'appel du constructeur, nous ne passons pas par ce rigamarole consistant à créer un nouveau temporaire, à l'initialiser et à le copier dans le local. Dans ce cas spécifique (et courant), nous pouvons optimiser la création du temporaire et de la copie car il est impossible pour un programme C # d'observer la différence! "
Jon Skeet
40

La mémoire contenant les champs d'une structure peut être allouée sur la pile ou le tas selon les circonstances. Si la variable de type struct est une variable ou un paramètre local qui n'est pas capturé par une classe de délégué ou d'itérateur anonyme, alors il sera alloué sur la pile. Si la variable fait partie d'une classe, elle sera allouée au sein de la classe sur le tas.

Si la structure est allouée sur le tas, l'appel du nouvel opérateur n'est pas réellement nécessaire pour allouer la mémoire. Le seul but serait de définir les valeurs de champ en fonction de ce qui se trouve dans le constructeur. Si le constructeur n'est pas appelé, tous les champs obtiendront leurs valeurs par défaut (0 ou null).

De même pour les structures allouées sur la pile, sauf que C # nécessite que toutes les variables locales soient définies sur une certaine valeur avant d'être utilisées, vous devez donc appeler soit un constructeur personnalisé soit le constructeur par défaut (un constructeur qui ne prend aucun paramètre est toujours disponible pour structures).

Jeffrey L Whitledge
la source
13

Pour le dire de façon compacte, new est un terme impropre pour les structures, appeler new appelle simplement le constructeur. Le seul emplacement de stockage pour la structure est l'emplacement où elle est définie.

S'il s'agit d'une variable membre, elle est stockée directement dans ce qu'elle est définie, si c'est une variable ou un paramètre local, elle est stockée dans la pile.

Comparez cela aux classes, qui ont une référence partout où la structure aurait été stockée dans son intégralité, tandis que la référence pointe quelque part sur le tas. (Membre à l'intérieur, local / paramètre sur la pile)

Il peut être utile de regarder un peu en C ++, où il n'y a pas de réelle distinction entre classe / struct. (Il existe des noms similaires dans la langue, mais ils se réfèrent uniquement à l'accessibilité par défaut des choses) Lorsque vous appelez new, vous obtenez un pointeur vers l'emplacement du tas, tandis que si vous avez une référence sans pointeur, elle est stockée directement sur la pile ou dans l'autre objet, ala structs en C #.

Guvante
la source
5

Comme avec tous les types de valeurs, les structures vont toujours là où elles ont été déclarées .

Voir cette question ici pour plus de détails sur l'utilisation des structures. Et cette question ici pour plus d'informations sur les structures.

Edit: j'avais par erreur répondu qu'ils vont TOUJOURS dans la pile. C'est incorrect .

Esteban Araya
la source
"les structures vont toujours là où elles ont été déclarées", c'est un peu déroutant et déroutant. Un champ struct dans une classe est toujours placé dans "la mémoire dynamique lorsqu'une instance du type est construite" - Jeff Richter. Cela peut être indirectement sur le tas, mais ce n'est pas du tout le même qu'un type de référence normal.
Ash
Non, je pense que c'est tout à fait exact - même si ce n'est pas la même chose qu'un type de référence. La valeur d'une variable vit là où elle est déclarée. La valeur d'une variable de type référence est une référence, au lieu des données réelles, c'est tout.
Jon Skeet
En résumé, chaque fois que vous créez (déclarez) un type de valeur n'importe où dans une méthode, il est toujours créé sur la pile.
Ash
2
Jon, tu me manques. La raison pour laquelle cette question a été posée pour la première fois est qu'il n'est pas clair pour de nombreux développeurs (moi inclus jusqu'à ce que je lise CLR via C #) où une structure est allouée si vous utilisez le nouvel opérateur pour le créer. Dire "les structures vont toujours là où elles ont été déclarées" n'est pas une réponse claire.
Ash
1
@Ash: Si j'ai le temps, je vais essayer de rédiger une réponse quand j'arriverai au travail. C'est un sujet trop gros pour essayer de le couvrir dans le train :)
Jon Skeet
4

Il me manque probablement quelque chose ici, mais pourquoi nous soucions-nous de l'allocation?

Les types de valeur sont passés par valeur;) et ne peuvent donc pas être mutés à une portée différente de celle où ils sont définis. Pour pouvoir muter la valeur, vous devez ajouter le mot clé [ref].

Les types de référence sont transmis par référence et peuvent être mutés.

Il existe bien sûr des chaînes de types de référence immuables qui sont les plus populaires.

Présentation / initialisation du tableau: types de valeurs -> mémoire zéro [nom, zip] [nom, zip] Types de référence -> mémoire zéro -> null [réf] [réf]

user18579
la source
3
Les types de référence ne sont pas transmis par référence - les références sont transmises par valeur. C'est très différent.
Jon Skeet
2

Une déclaration classor structest comme un plan directeur utilisé pour créer des instances ou des objets au moment de l'exécution. Si vous définissez une classou structappelée Personne, Personne est le nom du type. Si vous déclarez et initialisez une variable p de type Person, p est dit être un objet ou une instance de Person. Plusieurs instances du même type de personne peuvent être créées et chaque instance peut avoir des valeurs différentes dans ses propertieset fields.

A classest un type de référence. Lorsqu'un objet de classest créé, la variable à laquelle l'objet est affecté ne contient qu'une référence à cette mémoire. Lorsque la référence d'objet est affectée à une nouvelle variable, la nouvelle variable fait référence à l'objet d'origine. Les modifications apportées via une variable sont reflétées dans l'autre variable car elles font toutes deux référence aux mêmes données.

A structest un type de valeur. Lors de la structcréation de a, la variable à laquelle structest affecté contient les données réelles de la structure. Lorsque le structest affecté à une nouvelle variable, il est copié. La nouvelle variable et la variable d'origine contiennent donc deux copies distinctes des mêmes données. Les modifications apportées à une copie n'affectent pas l'autre copie.

En général, ils classessont utilisés pour modéliser des comportements plus complexes ou des données destinées à être modifiées après la classcréation d' un objet. Structsconviennent mieux aux petites structures de données qui contiennent principalement des données qui ne sont pas destinées à être modifiées après leur structcréation.

pour plus...

Sujit
la source
1

À peu près les structures qui sont considérées comme des types de valeur, sont allouées sur la pile, tandis que les objets sont alloués sur le tas, tandis que la référence d'objet (pointeur) est allouée sur la pile.

bashmohandes
la source
1

Les structures sont allouées à la pile. Voici une explication utile:

Structs

En outre, lorsqu'elles sont instanciées dans .NET, les classes allouent de la mémoire sur le tas ou l'espace mémoire réservé de .NET. Alors que les structures donnent plus d'efficacité lorsqu'elles sont instanciées en raison de l'allocation sur la pile. En outre, il convient de noter que les paramètres de passage dans les structures se font par valeur.

DaveK
la source
5
Cela ne couvre pas le cas où une structure fait partie d'une classe - à quel point elle vit sur le tas, avec le reste des données de l'objet.
Jon Skeet
1
Oui, mais il se concentre sur la question posée et y répond. A voté.
Ash
... tout en étant incorrect et trompeur. Désolé, mais il n'y a pas de réponse courte à cette question - Jeffrey's est la seule réponse complète.
Marc Gravell