Structs contre classes

93

Je suis sur le point de créer 100 000 objets dans le code. Ce sont des petits, avec seulement 2 ou 3 propriétés. Je vais les mettre dans une liste générique et quand ils le sont, je les boucle et vérifie la valeur aet peut-être la mise à jour b.

Est-il plus rapide / meilleur de créer ces objets en tant que classe ou en tant que struct?

ÉDITER

une. Les propriétés sont des types de valeur (sauf la chaîne je pense?)

b. Ils pourraient (nous ne sommes pas encore sûrs) avoir une méthode de validation

MODIFIER 2

Je me demandais: les objets sur le tas et la pile sont-ils traités de manière égale par le garbage collector, ou cela fonctionne-t-il différemment?

Michel
la source
2
Vont-ils seulement avoir des champs publics, ou vont-ils aussi avoir des méthodes? Les types sont-ils des types primitifs, tels que des entiers? Seront-ils contenus dans un tableau ou dans quelque chose comme List <T>?
JeffFerguson
14
Une liste de structures mutables? Attention au vélociraptor.
Anthony Pegram
1
@Anthony: j'ai peur de manquer la blague sur les vélociraptors: -s
Michel
5
La blague de velociraptor vient de XKCD. Mais quand vous jetez autour de l'idée fausse de `` les types de valeur sont alloués sur la pile '' / le détail de l'implémentation (supprimer le cas échéant), alors c'est Eric Lippert dont vous devez faire attention ...
Greg Beech
4
velociraptor: imgs.xkcd.com/comics/goto.png
WernerCD

Réponses:

137

Est-il plus rapide de créer ces objets en tant que classe ou en tant que struct?

Vous êtes la seule personne à pouvoir déterminer la réponse à cette question. Essayez les deux méthodes, mesurez une mesure de performance significative, centrée sur l'utilisateur et pertinente, puis vous saurez si le changement a un effet significatif sur les utilisateurs réels dans des scénarios pertinents.

Les structures consomment moins de mémoire de tas (parce qu'elles sont plus petites et plus facilement compactées, pas parce qu'elles sont «sur la pile»). Mais ils prennent plus de temps à copier qu'une copie de référence. Je ne sais pas quelles sont vos mesures de performance pour l'utilisation de la mémoire ou la vitesse; il y a un compromis ici et vous êtes la personne qui sait ce que c'est.

Vaut-il mieux créer ces objets en tant que classe ou en tant que struct?

Peut-être classe, peut-être struct. En règle générale: Si l'objet est:
1. Petit
2. Logiquement une valeur immuable
3. Il y en a beaucoup
Alors j'envisagerais d'en faire une structure. Sinon, je m'en tiendrai à un type de référence.

Si vous avez besoin de muter un champ d'une structure, il est généralement préférable de créer un constructeur qui renvoie une nouvelle structure entière avec le champ correctement défini. C'est peut-être un peu plus lent (mesurez-le!) Mais logiquement beaucoup plus facile à raisonner.

Les objets du tas et de la pile sont-ils traités de manière égale par le garbage collector?

Non , ils ne sont pas les mêmes car les objets de la pile sont les racines de la collection . Le ramasse-miettes n'a pas besoin de se demander "est-ce que cette chose sur la pile est vivante?" parce que la réponse à cette question est toujours "Oui, c'est sur la pile". (Maintenant, vous ne pouvez pas compter sur cela pour garder un objet en vie car la pile est un détail d'implémentation. La gigue est autorisée à introduire des optimisations qui, par exemple, enregistrent ce qui serait normalement une valeur de pile, et ensuite ce n'est jamais sur la pile donc le GC ne sait pas qu'il est toujours en vie. Un objet enregistré peut voir ses descendants collectés de manière agressive, dès que le registre qui le contient ne sera pas relu.)

Mais le garbage collector doit traiter les objets de la pile comme vivants, de la même manière qu'il traite tout objet connu comme vivant. L'objet sur la pile peut faire référence à des objets alloués au tas qui doivent être maintenus en vie, de sorte que le GC doit traiter les objets de pile comme des objets alloués au tas vivants aux fins de déterminer l'ensemble en direct. Mais évidemment, ils ne sont pas traités comme des "objets vivants" dans le but de compacter le tas, car ils ne sont pas sur le tas en premier lieu.

Est-ce clair?

Eric Lippert
la source
Eric, savez-vous si le compilateur ou la gigue utilise l'immuabilité (peut-être si elle est appliquée avec readonly) pour permettre des optimisations. Je ne laisserais pas cela affecter un choix sur la mutabilité (je suis un passionné des détails d'efficacité en théorie, mais en pratique, mon premier pas vers l'efficacité est toujours d'essayer d'avoir une garantie d'exactitude aussi simple que possible et donc de ne pas avoir à le faire. gaspiller les cycles du processeur et les cycles cérébraux sur les chèques et les cas de pointe, et le fait d'être correctement mutable ou immuable y contribue), mais cela contrerait toute réaction instinctive à votre affirmation que l'immuabilité peut être plus lente.
Jon Hanna
@Jon: Le compilateur C # optimise les données const mais pas les données en lecture seule . Je ne sais pas si le compilateur jit effectue des optimisations de mise en cache sur les champs en lecture seule.
Eric Lippert
Dommage, car je sais que la connaissance de l'immuabilité permet certaines optimisations, mais atteint les limites de mes connaissances théoriques à ce stade, mais ce sont des limites que j'aimerais étendre. En attendant "ça peut être plus rapide dans les deux sens, voici pourquoi, maintenant testez et découvrez ce qui s'applique dans ce cas" est utile pour pouvoir dire :)
Jon Hanna
Je recommanderais de lire simple-talk.com/dotnet/.net-framework / ... et votre propre article (@Eric): blogs.msdn.com/b/ericlippert/archive/2010/09/30 / ... pour commencer la plongée dans les détails. Il existe de nombreux autres bons articles. BTW, la différence dans le traitement de 100 000 petits objets en mémoire est à peine perceptible à travers une surcharge de mémoire (~ 2,3 Mo) pour la classe. Il peut être facilement vérifié par un simple test.
Nick Martyshchenko
Oui, c'est clair. Merci beaucoup pour votre réponse complète (étendue, c'est mieux? Google Translate a donné 2 traductions. Je voulais dire que vous avez pris le temps de ne pas écrire une réponse courte, mais que vous avez également pris le temps d'écrire tous les détails).
Michel
23

Parfois, structvous n'avez pas besoin d'appeler le constructeur new () et d'assigner directement les champs, ce qui le rend beaucoup plus rapide que d'habitude.

Exemple:

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i].id = i;
    list[i].isValid = true;
}

est environ 2 à 3 fois plus rapide que

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i] = new Value(i, true);
}

Valueest un structavec deux champs ( idet isValid).

struct Value
{
    int id;
    bool isValid;

    public Value(int i, bool isValid)
    {
        this.i = i;
        this.isValid = isValid;
    }
}

D'autre part, les éléments doivent être déplacés ou les types de valeur sélectionnés tout ce que la copie va vous ralentir. Pour obtenir la réponse exacte, je suppose que vous devez profiler votre code et le tester.

John Alexiou
la source
De toute évidence, les choses deviennent beaucoup plus rapides lorsque vous rassemblez les valeurs au-delà des limites natives.
leppie
Je suggère d'utiliser un nom autre que list, étant donné que le code indiqué ne fonctionnera pas avec un List<Value>.
supercat
7

Les structures peuvent sembler similaires aux classes, mais il existe des différences importantes dont vous devez être conscient. Tout d'abord, les classes sont des types référence et les structs sont des types valeur. En utilisant des structures, vous pouvez créer des objets qui se comportent comme les types intégrés et profiter de leurs avantages également.

Lorsque vous appelez l'opérateur New sur une classe, il sera alloué sur le tas. Cependant, lorsque vous instanciez une structure, elle est créée sur la pile. Cela entraînera des gains de performance. De plus, vous ne traiterez pas de références à une instance d'une structure comme vous le feriez avec des classes. Vous travaillerez directement avec l'instance struct. Pour cette raison, lors du passage d'un struct à une méthode, il est passé par valeur au lieu de comme référence.

Plus ici:

http://msdn.microsoft.com/en-us/library/aa288471(VS.71).aspx

kyndigs
la source
4
Je sais qu'il le dit sur MSDN, mais MSDN ne raconte pas toute l'histoire. Stack vs. heap est un détail d'implémentation et les structures ne vont pas toujours dans la pile. Pour un seul blog récent à ce sujet, voir: blogs.msdn.com/b/ericlippert/archive/2010/09/30/…
Anthony Pegram
"... c'est passé par valeur ..." les références et les structures sont passées par valeur (sauf si l'on utilise 'ref') - c'est si une valeur ou une référence est passée qui diffère, c'est-à-dire que les structures sont passées valeur par valeur , les objets de classe sont passés référence par valeur et les paramètres marqués par référence passent référence par référence.
Paul Ruane
10
Cet article est trompeur sur plusieurs points clés, et j'ai demandé à l'équipe MSDN de le réviser ou de le supprimer.
Eric Lippert
2
@supercat: pour adresser votre premier point: le point le plus important est que dans le code managé où une valeur ou une référence à une valeur est stockée est en grande partie sans importance . Nous avons travaillé dur pour créer un modèle de mémoire qui, la plupart du temps, permet aux développeurs de permettre au moteur d'exécution de prendre des décisions de stockage intelligentes en leur nom. Ces distinctions importent beaucoup quand le fait de ne pas les comprendre a des conséquences écrasantes comme c'est le cas en C; pas tellement en C #.
Eric Lippert
1
@supercat: pour répondre à votre deuxième point, aucune structure modifiable n'est généralement mauvaise. Par exemple, void M () {S s = new S (); s.Blah (); N (s); }. Refactoriser en: void DoBlah (S) {s.Blah (); } void M (S s = new S (); DoBlah (s); N (s);}. Cela vient d'introduire un bogue car S est une structure mutable. Avez-vous immédiatement vu le bogue? Ou est-ce que S est une structure mutable vous cache le bogue?
Eric Lippert
6

Les tableaux de structures sont représentés sur le tas dans un bloc contigu de mémoire, tandis qu'un tableau d'objets est représenté comme un bloc contigu de références avec les objets réels eux-mêmes ailleurs sur le tas, nécessitant ainsi de la mémoire pour les objets et pour leurs références de tableau .

Dans ce cas, comme vous les placez dans a List<>(et que a List<>est sauvegardé sur un tableau), il serait plus efficace, en termes de mémoire, d'utiliser des structures.

(Attention cependant, les grands tableaux trouveront leur chemin sur le tas d'objets volumineux où, si leur durée de vie est longue, ils peuvent avoir un effet négatif sur la gestion de la mémoire de votre processus. N'oubliez pas non plus que la mémoire n'est pas la seule considération.)

Paul Ruane
la source
Vous pouvez utiliser des refmots clés pour gérer cela.
leppie
"Attention cependant, les grands tableaux trouveront leur chemin sur le tas d'objets volumineux où, si leur durée de vie est longue, ils peuvent avoir un effet négatif sur la gestion de la mémoire de votre processus." - Je ne sais pas trop pourquoi tu penses ça? Être alloué sur le LOH n'entraînera aucun effet indésirable sur la gestion de la mémoire à moins (éventuellement) qu'il s'agisse d'un objet de courte durée et que vous souhaitiez récupérer la mémoire rapidement sans attendre une collection Gen 2.
Jon Artus du
@Jon Artus: le LOH ne se compacte pas. Tout objet de longue durée divisera le LOH en zone de mémoire libre avant et en zone après. Une mémoire contiguë est requise pour l'allocation et si ces zones ne sont pas assez grandes pour une allocation, alors plus de mémoire est allouée au LOH (c'est-à-dire que vous obtiendrez une fragmentation LOH).
Paul Ruane
4

S'ils ont une sémantique de valeur, vous devriez probablement utiliser une structure. S'ils ont une sémantique de référence, vous devriez probablement utiliser une classe. Il existe des exceptions, qui penchent principalement vers la création d'une classe même lorsqu'il existe une sémantique de valeur, mais à partir de là.

Quant à votre deuxième édition, le GC ne traite que du tas, mais il y a beaucoup plus d'espace de tas que d'espace de pile, donc mettre des choses sur la pile n'est pas toujours une victoire. En plus de cela, une liste de struct-types et une liste de class-types seront sur le tas de toute façon, donc ce n'est pas pertinent dans ce cas.

Éditer:

Je commence à considérer le terme mal comme nocif. Après tout, rendre une classe mutable est une mauvaise idée si elle n'est pas activement nécessaire, et je n'exclurais jamais d'utiliser une structure mutable. C'est une mauvaise idée si souvent qu'elle est presque toujours une mauvaise idée, mais la plupart du temps, cela ne coïncide pas avec la sémantique des valeurs, donc cela n'a tout simplement pas de sens d'utiliser une structure dans le cas donné.

Il peut y avoir des exceptions raisonnables avec les structures imbriquées privées, où toutes les utilisations de cette structure sont donc limitées à une portée très limitée. Cela ne s'applique pas ici cependant.

Vraiment, je pense que "ça mute donc c'est une mauvaise structure" n'est pas beaucoup mieux que de parler du tas et de la pile (ce qui a au moins un impact sur les performances, même si celui-ci est souvent déformé). "Il mute, donc cela n'a probablement pas de sens de le considérer comme ayant une sémantique de valeur, donc c'est une mauvaise structure" n'est que légèrement différent, mais surtout je pense.

Jon Hanna
la source
3

La meilleure solution est de mesurer, mesurer à nouveau, puis mesurer un peu plus. Il peut y avoir des détails sur ce que vous faites qui peuvent rendre difficile une réponse simplifiée et facile comme «utiliser des structures» ou «utiliser des classes».

FMM
la source
d'accord avec la partie mesure, mais à mon avis, c'était un exemple simple et clair, et j'ai pensé que peut-être des choses génériques pourraient être dites à ce sujet. Et il s'est avéré que certaines personnes l'ont fait.
Michel
3

Une structure n'est, en son cœur, ni plus ni moins qu'une agrégation de champs. Dans .NET, il est possible pour une structure de "faire semblant" d'être un objet, et pour chaque type de structure .NET définit implicitement un type d'objet de tas avec les mêmes champs et méthodes qui - étant un objet de tas - se comporteront comme un objet . Une variable qui contient une référence à un tel objet de tas (structure "en boîte") présentera une sémantique de référence, mais celle qui contient directement une structure est simplement une agrégation de variables.

Je pense qu'une grande partie de la confusion struct-versus-class provient du fait que les structures ont deux cas d'utilisation très différents, qui devraient avoir des directives de conception très différentes, mais les directives MS ne font pas de distinction entre elles. Parfois, il y a un besoin de quelque chose qui se comporte comme un objet; dans ce cas, les directives MS sont assez raisonnables, bien que la "limite de 16 octets" devrait probablement être plus comme 24-32. Parfois, cependant, il faut une agrégation de variables. Une structure utilisée à cette fin devrait simplement consister en un ensemble de champs publics, et éventuellement un Equalsremplacement, un ToStringremplacement etIEquatable(itsType).Equalsla mise en oeuvre. Les structures qui sont utilisées comme agrégations de champs ne sont pas des objets et ne doivent pas prétendre l'être. Du point de vue de la structure, la signification du champ ne doit être ni plus ni moins que «la dernière chose écrite dans ce champ». Toute signification supplémentaire doit être déterminée par le code client.

Par exemple, si une structure d'agrégation de variables a des membres Minimumet Maximum, la structure elle-même ne doit pas le promettre Minimum <= Maximum. Le code qui reçoit une telle structure en tant que paramètre doit se comporter comme s'il avait été transmis séparément Minimumet Maximumvaleurs. Une exigence qui Minimumn'est pas supérieure à Maximumdoit être considérée comme une exigence selon laquelle un Minimumparamètre n'est pas supérieur à un paramètre passé séparément Maximum.

Un modèle utile à considérer parfois est d'avoir une ExposedHolder<T>classe définie quelque chose comme:

class ExposedHolder<T>
{
  public T Value;
  ExposedHolder() { }
  ExposedHolder(T val) { Value = T; }
}

Si on a un List<ExposedHolder<someStruct>>, où someStructest une structure d'agrégation de variables, on peut faire des choses comme myList[3].Value.someField += 7;, mais donner myList[3].Valueà un autre code lui donnera le contenu Valueplutôt que de lui donner un moyen de le modifier. En revanche, si on utilisait a List<someStruct>, il faudrait utiliser var temp=myList[3]; temp.someField += 7; myList[3] = temp;. Si l'on utilisait un type de classe mutable, exposer le contenu de myList[3]à un code extérieur nécessiterait de copier tous les champs vers un autre objet. Si l'on utilisait un type de classe immuable, ou une structure "de style objet", il serait nécessaire de construire une nouvelle instance qui ressemblerait à l' myList[3]exception de celle someFieldqui était différente, puis de stocker cette nouvelle instance dans la liste.

Une note supplémentaire: si vous stockez un grand nombre de choses similaires, il peut être bon de les stocker dans des tableaux de structures éventuellement imbriqués, en essayant de préférence de garder la taille de chaque tableau entre 1K et 64K environ. Les tableaux de structures sont spéciaux, en ce que l'indexation donnera une référence directe à une structure à l'intérieur, donc on peut dire "a [12] .x = 5;". Bien que l'on puisse définir des objets de type tableau, C # ne leur permet pas de partager une telle syntaxe avec des tableaux.

supercat
la source
1

Utilisez des cours.

Sur une note générale. Pourquoi ne pas mettre à jour la valeur b au fur et à mesure que vous les créez?

Preet Sangha
la source
1

D'un point de vue c ++, je suis d'accord qu'il sera plus lent de modifier les propriétés d'une structure par rapport à une classe. Mais je pense qu'ils seront plus rapides à lire en raison de l'allocation de la structure sur la pile au lieu du tas. La lecture des données à partir du tas nécessite plus de contrôles qu'à partir de la pile.

Robert
la source
1

Eh bien, si vous optez pour struct après tout, supprimez la chaîne et utilisez un tampon de caractères ou d'octets de taille fixe.

C'est re: la performance.

Daniel Mošmondor
la source