Pourquoi le nouveau type Tuple dans .Net 4.0 est-il un type de référence (classe) et non un type valeur (struct)


88

Quelqu'un connaît-il la réponse et / ou a-t-il une opinion à ce sujet?

Puisque les tuples ne seraient normalement pas très grands, je suppose qu'il serait plus logique d'utiliser des structures que des classes pour ces derniers. Ce que vous dites?


1
Pour tous ceux qui trébuchent ici après 2016. En c # 7 et plus récent, les littéraux Tuple sont de la famille de types ValueTuple<...>. Voir la référence sur les types de tuple C #
Tamir Daniely

Réponses:


93

Microsoft a créé tous les types de références de types de tuple dans un souci de simplicité.

Je pense personnellement que c'était une erreur. Les tuples avec plus de 4 champs sont très inhabituels et devraient de toute façon être remplacés par une alternative plus typée (comme un type d'enregistrement en F #) donc seuls les petits tuples sont d'un intérêt pratique. Mes propres tests de performance ont montré que les tuples sans boîte jusqu'à 512 octets pouvaient toujours être plus rapides que les tuples en boîte.

Bien que l'efficacité de la mémoire soit une préoccupation, je pense que le problème dominant est la surcharge du garbage collector .NET. L'allocation et la collecte sont très coûteuses sur .NET car son garbage collector n'a pas été très fortement optimisé (par exemple par rapport à la JVM). De plus, le .NET GC (poste de travail) par défaut n'a pas encore été parallélisé. Par conséquent, les programmes parallèles qui utilisent des tuples s'arrêtent alors que tous les cœurs se disputent le garbage collector partagé, détruisant l'évolutivité. Ce n'est pas seulement la préoccupation dominante mais, AFAIK, a été complètement négligé par Microsoft quand ils ont examiné ce problème.

Une autre préoccupation est la répartition virtuelle. Les types de référence prennent en charge les sous-types et, par conséquent, leurs membres sont généralement appelés via une distribution virtuelle. En revanche, les types valeur ne peuvent pas prendre en charge les sous-types, de sorte que l'invocation de membre est sans ambiguïté et peut toujours être effectuée comme un appel de fonction direct. La distribution virtuelle est extrêmement coûteuse sur le matériel moderne car le processeur ne peut pas prédire où le compteur de programme se terminera. La JVM fait de grands efforts pour optimiser la répartition virtuelle, mais pas .NET. Cependant, .NET fournit une évasion de la distribution virtuelle sous la forme de types valeur. Donc, représenter les tuples comme des types valeur pourrait, encore une fois, avoir considérablement amélioré les performances ici. Par exemple, appelerGetHashCode sur un 2-tuple un million de fois prend 0,17 s, mais l'appeler sur une structure équivalente ne prend que 0,008 s, c'est-à-dire que le type valeur est 20 fois plus rapide que le type référence.

Une situation réelle où ces problèmes de performances avec les tuples surviennent fréquemment est l'utilisation de tuples comme clés dans les dictionnaires. En fait, je suis tombé sur ce fil en suivant un lien de la question Stack Overflow F # exécute mon algorithme plus lentement que Python! où le programme F # de l'auteur s'est avéré être plus lent que son Python précisément parce qu'il utilisait des tuples encadrés. Le déballage manuel à l'aide d'un structtype écrit à la main rend son programme F # plusieurs fois plus rapide et plus rapide que Python. Ces problèmes ne se seraient jamais posés si les tuples étaient représentés par des types valeur et non par des types référence pour commencer ...


2
@Bent: Oui, c'est exactement ce que je fais quand je rencontre des tuples sur un chemin chaud en F #. Ce serait bien s'ils avaient fourni à la fois des tuples en boîte et sans boîte dans le .NET Framework ...
JD

18
En ce qui concerne l'envoi virtuel, je pense que votre blâme est déplacé: les Tuple<_,...,_>types auraient pu être scellés, auquel cas aucun envoi virtuel ne serait nécessaire bien qu'il s'agisse de types de référence. Je suis plus curieux de savoir pourquoi ils ne sont pas scellés que de savoir pourquoi ce sont des types de référence.
kvb

2
D'après mes tests, pour le scénario dans lequel un tuple serait généré dans une fonction et renvoyé à une autre fonction, puis plus jamais utilisé, les structures de champ exposé semblent offrir des performances supérieures pour tout élément de données de taille qui n'est pas si énorme la pile. Les classes immuables ne sont meilleures que si les références sont suffisamment transmises pour justifier leur coût de construction (plus la donnée est grande, moins elles doivent être passées en revue pour que le compromis les favorise). Puisqu'un tuple est censé représenter simplement un groupe de variables collées ensemble, une structure semble idéale.
supercat

2
"Les tuples sans boîte jusqu'à 512 octets pourraient encore être plus rapides qu'en boîte" - de quel scénario s'agit-il? Vous pourriez être en mesure d'allouer une structure de 512B plus rapidement qu'une instance de classe contenant 512B de données, mais la transmettre serait plus de 100 fois plus lente (en supposant x86). Y a-t-il quelque chose que je néglige?
Groo


45

La raison est très probablement parce que seuls les plus petits tuples auraient du sens en tant que types valeur car ils auraient une petite empreinte mémoire. Les plus gros tuples (c'est-à-dire ceux avec plus de propriétés) souffriraient en fait de la performance puisqu'ils seraient plus grands que 16 octets.

Plutôt que de faire en sorte que certains tuples soient des types valeur et que d'autres soient des types de référence et obligent les développeurs à savoir lesquels, j'imagine que les gens de Microsoft ont pensé que faire d'eux tous les types de référence était plus simple.

Ah, soupçons confirmés! S'il vous plaît voir Building Tuple :

La première décision majeure était de savoir s'il fallait traiter les tuples comme un type de référence ou de valeur. Puisqu'ils sont immuables à chaque fois que vous souhaitez modifier les valeurs d'un tuple, vous devez en créer un nouveau. S'il s'agit de types référence, cela signifie qu'il peut y avoir beaucoup de déchets générés si vous modifiez des éléments dans un tuple dans une boucle serrée. Les tuples F # étaient des types de référence, mais l'équipe avait le sentiment qu'elle pouvait améliorer les performances si deux, et peut-être trois, tuples d'éléments étaient des types valeur à la place. Certaines équipes qui avaient créé des tuples internes avaient utilisé la valeur au lieu des types référence, car leurs scénarios étaient très sensibles à la création de nombreux objets gérés. Ils ont constaté que l'utilisation d'un type valeur leur donnait de meilleures performances. Dans notre premier projet de spécification de tuple, nous avons conservé les tuples à deux, trois et quatre éléments comme types valeur, le reste étant des types référence. Cependant, lors d'une réunion de conception qui incluait des représentants d'autres langues, il a été décidé que cette conception «scindée» serait déroutante, en raison de la sémantique légèrement différente entre les deux types. La cohérence du comportement et de la conception a été jugée plus prioritaire que les augmentations potentielles des performances. Sur la base de cette entrée, nous avons modifié la conception afin que tous les tuples soient des types de référence, bien que nous ayons demandé à l'équipe F # de faire une enquête sur les performances pour voir si elle rencontrait une accélération lors de l'utilisation d'un type valeur pour certaines tailles de tuples. Il avait un bon moyen de tester cela, puisque son compilateur, écrit en F #, était un bon exemple d'un grand programme qui utilisait des tuples dans une variété de scénarios. En fin de compte, l'équipe F # a constaté qu'elle n'obtenait pas d'amélioration des performances lorsque certains tuples étaient des types valeur au lieu de types référence. Cela nous a permis de nous sentir mieux dans notre décision d'utiliser des types de référence pour le tuple.



Ahh je vois. Je suis encore un peu confus que les types de valeur ne signifient rien dans la pratique ici: P
Bent Rasmussen

Je viens de lire le commentaire sur l'absence d'interfaces génériques et en regardant le code plus tôt, c'était exactement une autre chose qui m'a frappé. Il n'est vraiment pas intéressant de voir à quel point les types Tuple sont non génériques. Mais, je suppose que vous pouvez toujours créer le vôtre ... Il n'y a de toute façon pas de support syntaxique en C #. Pourtant au moins ... Pourtant, l'utilisation des génériques et les contraintes qu'il a se sentent encore limitées limitées dans .Net. Il existe un potentiel substantiel pour des bibliothèques très abstraites très génériques, mais les génériques ont probablement besoin de choses supplémentaires comme les types de retour covariants.
Bent Rasmussen

7
Votre limite de «16 octets» est fausse. Lorsque j'ai testé cela sur .NET 4, j'ai trouvé que le GC est si lent que les tuples sans boîte jusqu'à 512 octets peuvent encore être plus rapides. Je remettrais également en question les résultats de référence de Microsoft. Je parie qu'ils ont ignoré le parallélisme (le compilateur F # n'est pas parallèle) et c'est là que le fait d'éviter GC est vraiment rentable car le poste de travail GC de .NET n'est pas non plus parallèle.
JD

Par curiosité, je me demande si l'équipe du compilateur a testé l'idée de faire des tuples des structures EXPOSED-FIELD ? Si l'on a une instance d'un type avec divers traits, et a besoin d'une instance qui est identique sauf pour un trait qui est différent, une structure de champ exposé peut accomplir cela beaucoup plus rapidement que tout autre type, et l'avantage ne grandit que lorsque les structures obtiennent plus gros.
supercat du

7

Si les types .NET System.Tuple <...> étaient définis comme des structures, ils ne seraient pas évolutifs. Par exemple, un tuple ternaire d'entiers longs est actuellement mis à l'échelle comme suit:

type Tuple3 = System.Tuple<int64, int64, int64>
type Tuple33 = System.Tuple<Tuple3, Tuple3, Tuple3>
sizeof<Tuple3> // Gets 4
sizeof<Tuple33> // Gets 4

Si le tuple ternaire était défini en tant que struct, le résultat serait le suivant (basé sur un exemple de test que j'ai implémenté):

sizeof<Tuple3> // Would get 32
sizeof<Tuple33> // Would get 104

Comme les tuples ont un support de syntaxe intégré en F #, et qu'ils sont utilisés très souvent dans ce langage, les tuples "struct" exposeraient les programmeurs F # au risque d'écrire des programmes inefficaces sans même en être conscients. Cela arriverait si facilement:

let t3 = 1L, 2L, 3L
let t33 = t3, t3, t3

À mon avis, les tuples «struct» entraîneraient une forte probabilité de créer des inefficacités significatives dans la programmation quotidienne. D'autre part, les tuples de "classe" actuellement existants provoquent également certaines inefficacités, comme mentionné par @Jon. Cependant, je pense que le produit de la "probabilité d'occurrence" par le "dommage potentiel" serait beaucoup plus élevé avec les structures qu'il ne l'est actuellement avec les classes. Par conséquent, la mise en œuvre actuelle est le moindre mal.

Idéalement, il y aurait à la fois des tuples "class" et des tuples "struct", tous deux avec support syntaxique en F #!

Modifier (2017-10-07)

Les tuples de structure sont désormais entièrement pris en charge comme suit:

  • Construit dans mscorlib (.NET> = 4.7) en tant que System.ValueTuple
  • Disponible en tant que NuGet pour les autres versions
  • Prise en charge syntaxique en C #> = 7
  • Prise en charge syntaxique en F #> = 4.1

2
Si l'on évite une copie inutile, une structure de champ exposé de n'importe quelle taille sera plus efficace qu'une classe immuable de même taille, à moins que chaque instance ne soit copiée suffisamment de fois pour que le coût d'une telle copie surmonte le coût de création d'un objet de tas (le le seuil de rentabilité des copies varie en fonction de la taille de l'objet). Une telle copie peut être inévitable si l'on veut une structure qui prétend être immuable, mais les structures qui sont conçues pour apparaître comme des collections de variables (ce qui est ce que sont les structures ) peuvent être utilisées efficacement même lorsqu'elles sont énormes.
supercat

2
Il se peut que F # ne joue pas bien avec l'idée de passer des structures par ref, ou peut ne pas aimer le fait que les soi-disant «structures immuables» ne le sont pas, surtout lorsqu'elles sont encadrées. C'est dommage que .net n'ait jamais implémenté le concept de passage de paramètres par un exécutable const ref, car dans de nombreux cas, une telle sémantique est vraiment nécessaire.
supercat

1
Incidemment, je considère le coût amorti de GC comme faisant partie du coût d'attribution des objets; si un GC L0 était nécessaire après chaque mégaoctet d'allocations, alors le coût d'allocation de 64 octets est d'environ 1/16 000 du coût d'un GC L0, plus une fraction du coût de tout GC L1 ou L2 qui devient nécessaire en tant que conséquence de celui-ci.
supercat du

4
"Je pense que le produit de la probabilité d'occurrence par les dommages potentiels serait beaucoup plus élevé avec les structures qu'il ne l'est actuellement avec les classes". FWIW, j'ai très rarement vu des tuples de tuples dans la nature et je les considère comme un défaut de conception, mais je vois très souvent des gens lutter avec des performances horribles lorsqu'ils utilisent des tuples (ref) comme clés dans un Dictionary, par exemple ici: stackoverflow.com/questions/5850243 /…
JD

3
@Jon Cela fait deux ans que j'ai écrit cette réponse, et je suis maintenant d'accord avec vous qu'il serait préférable qu'au moins 2 et 3 tuples soient des structures. Une suggestion vocale d'utilisateur en langage F # a été faite à cet égard. Le problème est urgent, car il y a eu une croissance massive des applications dans les mégadonnées, la finance quantitative et les jeux ces dernières années.
Marc Sigrist

4

Pour 2-tuples, vous pouvez toujours utiliser le KeyValuePair <TKey, TValue> des versions antérieures du Common Type System. C'est un type de valeur.

Une clarification mineure à l'article de Matt Ellis serait que la différence de sémantique d'utilisation entre les types référence et valeur n'est "légère" que lorsque l'immuabilité est en vigueur (ce qui, bien sûr, serait le cas ici). Néanmoins, je pense qu'il aurait été préférable dans la conception BCL de ne pas introduire la confusion du passage de Tuple à un type de référence à un certain seuil.


Si une valeur sera utilisée une fois après son retour, une structure de champ exposé de n'importe quelle taille surpassera tout autre type, à condition seulement qu'elle ne soit pas monstrueusement énorme pour faire exploser la pile. Le coût de création d'un objet de classe ne sera récupéré que si la référence finit par être partagée plusieurs fois. Il y a des moments où il est utile pour un type hétérogène de taille fixe à usage général d'être une classe, mais il y a d'autres moments où une structure serait meilleure - même pour de «grandes» choses.
supercat du

Merci d'avoir ajouté cette règle de base utile. J'espère cependant que vous n'avez pas mal compris ma position: je suis un drogué de valeur. ( stackoverflow.com/a/14277068 ne devrait laisser aucun doute).
Glenn Slayden

Les types de valeur sont l'une des grandes fonctionnalités de .net, mais malheureusement, la personne qui a rédigé le msdn dox n'a pas reconnu qu'il y avait plusieurs cas d'utilisation disjoints pour eux, et que différents cas d'utilisation devraient avoir des directives différentes. Le style de struct msdn recommande ne doit être utilisé qu'avec des structs qui représentent une valeur homogène, mais si l'on a besoin de représenter des valeurs indépendantes attachées avec du ruban adhésif, il ne faut pas utiliser ce style de struct - on devrait utiliser un struct avec terrains publics exposés.
supercat

0

Je ne sais pas mais si vous avez déjà utilisé F # Les tuples font partie du langage. Si j'ai créé un .dll et renvoyé un type de tuples, ce serait bien d'avoir un type pour le mettre. Je soupçonne maintenant que F # fait partie du langage (.Net 4), des modifications ont été apportées à CLR pour accueillir certaines structures communes en fa #

Depuis http://en.wikibooks.org/wiki/F_Sharp_Programming/Tuples_and_Records

let scalarMultiply (s : float) (a, b, c) = (a * s, b * s, c * s);;

val scalarMultiply : float -> float * float * float -> float * float * float

scalarMultiply 5.0 (6.0, 10.0, 20.0);;
val it : float * float * float = (30.0, 50.0, 100.0)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.