Warum ist der neue Tupeltyp in .Net 4.0 ein Referenztyp (Klasse) und kein Werttyp (Struktur)?

88

Kennt jemand die Antwort und / oder hat er eine Meinung dazu?

Da Tupel normalerweise nicht sehr groß sind, würde ich annehmen, dass es sinnvoller wäre, für diese Strukturen als Klassen zu verwenden. Was sagst du?

Bent Rasmussen
quelle
1
Für alle, die nach 2016 hier stolpern. In c # 7 und neueren gehören Tupel-Literale zur Typfamilie ValueTuple<...>. Siehe Referenz bei C #
Tupeltypen

Antworten:

93

Microsoft hat aus Gründen der Einfachheit alle Tupeltypen als Referenztypen festgelegt.

Ich persönlich halte das für einen Fehler. Tupel mit mehr als 4 Feldern sind sehr ungewöhnlich und sollten ohnehin durch eine typvollere Alternative ersetzt werden (z. B. ein Datensatztyp in F #), sodass nur kleine Tupel von praktischem Interesse sind. Meine eigenen Benchmarks haben gezeigt, dass Tupel ohne Box bis zu 512 Byte immer noch schneller sein können als Tupel mit Box.

Obwohl die Speichereffizienz ein Problem darstellt, glaube ich, dass das Hauptproblem der Overhead des .NET-Garbage-Collectors ist. Zuweisung und Erfassung sind in .NET sehr teuer, da der Garbage Collector nicht sehr stark optimiert wurde (z. B. im Vergleich zur JVM). Darüber hinaus wurde die Standard-.NET-GC (Workstation) noch nicht parallelisiert. Folglich kommen parallele Programme, die Tupel verwenden, zum Stillstand, da alle Kerne um den gemeinsam genutzten Garbage Collector kämpfen, wodurch die Skalierbarkeit zerstört wird. Dies ist nicht nur das Hauptanliegen, sondern AFAIK wurde von Microsoft bei der Untersuchung dieses Problems völlig vernachlässigt.

Ein weiteres Problem ist der virtuelle Versand. Referenztypen unterstützen Untertypen und daher werden ihre Mitglieder normalerweise über den virtuellen Versand aufgerufen. Im Gegensatz dazu können Werttypen keine Untertypen unterstützen, sodass der Aufruf von Mitgliedern völlig eindeutig ist und immer als direkter Funktionsaufruf ausgeführt werden kann. Der virtuelle Versand ist auf moderner Hardware sehr teuer, da die CPU nicht vorhersagen kann, wo der Programmzähler landen wird. Die JVM unternimmt große Anstrengungen, um den virtuellen Versand zu optimieren, .NET jedoch nicht. .NET bietet jedoch eine Flucht vor dem virtuellen Versand in Form von Werttypen. Die Darstellung von Tupeln als Werttypen hätte die Leistung hier wiederum dramatisch verbessern können. Zum Beispiel anrufenGetHashCode Bei einem 2-Tupel dauert eine Million Mal 0,17 Sekunden, beim Aufrufen einer äquivalenten Struktur jedoch nur 0,008 Sekunden, dh der Werttyp ist 20-mal schneller als der Referenztyp.

Eine reale Situation, in der diese Leistungsprobleme bei Tupeln häufig auftreten, ist die Verwendung von Tupeln als Schlüssel in Wörterbüchern. Ich bin tatsächlich auf diesen Thread gestoßen, indem ich einem Link aus der Frage zum Stapelüberlauf gefolgt bin. F # führt meinen Algorithmus langsamer als Python aus! wo sich herausstellte, dass das F # -Programm des Autors langsamer war als sein Python, gerade weil er Boxed-Tupel verwendete. Durch manuelles Entpacken mit einem handgeschriebenen structTyp wird sein F # -Programm um ein Vielfaches schneller und schneller als Python. Diese Probleme wären niemals aufgetreten, wenn Tupel zunächst durch Werttypen und nicht durch Referenztypen dargestellt worden wären ...

JD
quelle
2
@Bent: Ja, genau das mache ich, wenn ich auf einem heißen Pfad in F # auf Tupel stoße. Wäre schön, wenn sie sowohl Tupel mit als auch ohne Box in .NET Framework bereitgestellt hätten ...
JD
18
In Bezug auf den virtuellen Versand denke ich, dass Ihre Schuld falsch ist: Die Tuple<_,...,_>Typen könnten versiegelt worden sein. In diesem Fall wäre kein virtueller Versand erforderlich, obwohl es sich um Referenztypen handelt. Ich bin neugieriger, warum sie nicht versiegelt sind, als warum sie Referenztypen sind.
KVB
2
Nach meinen Tests scheinen für das Szenario, in dem ein Tupel in einer Funktion generiert und zu einer anderen Funktion zurückgegeben und dann nie wieder verwendet wird, exponierte Feldstrukturen eine überlegene Leistung für Datenelemente jeder Größe zu bieten , die nicht so groß sind, dass sie blasen der Stapel. Unveränderliche Klassen sind nur dann besser, wenn die Referenzen so weit herumgereicht werden, dass ihre Baukosten gerechtfertigt sind (je größer das Datenelement, desto weniger müssen sie weitergegeben werden, damit der Kompromiss sie begünstigt). Da ein Tupel einfach eine Reihe von zusammengeklebten Variablen darstellen soll, erscheint eine Struktur ideal.
Supercat
2
"Unboxed Tupel bis zu 512 Bytes könnten immer noch schneller sein als Boxed" - welches Szenario ist das? Sie könnten in der Lage eine 512B Struktur schneller als eine Instanz der Klasse zuzuteilen 512B von Daten zu halten, aber es um vorbei wäre mehr als 100 - mal langsamer (x86 vorausgesetzt). Gibt es etwas, das ich übersehen habe?
Groo
44

Der Grund ist höchstwahrscheinlich, dass nur die kleineren Tupel als Werttypen sinnvoll wären, da sie einen geringen Speicherbedarf hätten. Die größeren Tupel (dh diejenigen mit mehr Eigenschaften) würden tatsächlich an Leistung verlieren, da sie größer als 16 Bytes wären.

Anstatt einige Tupel Werttypen und andere Referenztypen zu sein, müssen Entwickler wissen, welche es sind. Ich würde mir vorstellen, dass die Leute bei Microsoft es für einfacher hielten, sie alle Referenztypen zu erstellen.

Ah, Verdacht bestätigt! Siehe Building Tuple :

Die erste wichtige Entscheidung war, ob Tupel entweder als Referenz- oder als Werttyp behandelt werden sollen. Da sie jedes Mal unveränderlich sind, wenn Sie die Werte eines Tupels ändern möchten, müssen Sie einen neuen erstellen. Wenn es sich um Referenztypen handelt, bedeutet dies, dass viel Müll generiert werden kann, wenn Sie Elemente in einem Tupel in einer engen Schleife ändern. F # -Tupel waren Referenztypen, aber das Team hatte das Gefühl, dass sie eine Leistungsverbesserung erzielen könnten, wenn stattdessen zwei und vielleicht drei Elementtupel Werttypen wären. Einige Teams, die interne Tupel erstellt hatten, verwendeten Wert anstelle von Referenztypen, da ihre Szenarien sehr empfindlich auf die Erstellung vieler verwalteter Objekte reagierten. Sie fanden heraus, dass die Verwendung eines Wertetyps zu einer besseren Leistung führte. In unserem ersten Entwurf der Tupelspezifikation Wir haben die Tupel mit zwei, drei und vier Elementen als Werttypen beibehalten, der Rest sind Referenztypen. Während eines Designtreffens, an dem Vertreter anderer Sprachen teilnahmen, wurde jedoch entschieden, dass dieses "geteilte" Design aufgrund der leicht unterschiedlichen Semantik zwischen den beiden Typen verwirrend sein würde. Es wurde festgestellt, dass Konsistenz in Verhalten und Design eine höhere Priorität hat als potenzielle Leistungssteigerungen. Basierend auf dieser Eingabe haben wir das Design so geändert, dass alle Tupel Referenztypen sind, obwohl wir das F # -Team gebeten haben, eine Leistungsuntersuchung durchzuführen, um festzustellen, ob bei Verwendung eines Wertetyps für einige Tupelgrößen eine Beschleunigung aufgetreten ist. Es hatte eine gute Möglichkeit, dies zu testen, da sein Compiler, geschrieben in F #, war ein gutes Beispiel für ein großes Programm, das Tupel in verschiedenen Szenarien verwendete. Am Ende stellte das F # -Team fest, dass es keine Leistungsverbesserung erzielte, wenn einige Tupel Werttypen anstelle von Referenztypen waren. Dadurch fühlten wir uns besser bei unserer Entscheidung, Referenztypen für Tupel zu verwenden.

Andrew Hare
quelle
3
Tolle Diskussion hier: blogs.msdn.com/bclteam/archive/2009/07/07/…
Keith Adler
Ahh, ich verstehe. Ich bin immer noch ein wenig verwirrt, dass
Bent Rasmussen
Ich habe gerade den Kommentar über keine generischen Schnittstellen gelesen und als ich mir den Code früher ansah, war das genau eine andere Sache, die mich beeindruckt hat. Es ist wirklich ziemlich langweilig, wie ungenerisch die Tupel-Typen sind. Aber ich denke, Sie können immer Ihre eigenen erstellen ... In C # gibt es sowieso keine syntaktische Unterstützung. Zumindest ... Trotzdem ist der Einsatz von Generika und die damit verbundenen Einschränkungen in .Net immer noch begrenzt. Es gibt ein erhebliches Potenzial für sehr generische, sehr abstrakte Bibliotheken, aber Generika benötigen wahrscheinlich zusätzliche Dinge wie kovariante Rückgabetypen.
Bent Rasmussen
7
Ihr "16-Byte" -Limit ist falsch. Als ich dies auf .NET 4 getestet habe, habe ich festgestellt, dass der GC so langsam ist, dass nicht verpackte Tupel mit bis zu 512 Byte immer noch schneller sein können. Ich würde auch die Benchmark-Ergebnisse von Microsoft in Frage stellen. Ich wette, sie haben die Parallelität ignoriert (der F # -Compiler ist nicht parallel), und hier zahlt es sich wirklich aus, GC zu vermeiden, da die GC der Workstation von .NET ebenfalls nicht parallel ist.
JD
Aus Neugier frage ich mich, ob das Compilerteam die Idee getestet hat, Tupel zu EXPOSED-FIELD- Strukturen zu machen. Wenn man eine Instanz eines Typs mit verschiedenen Merkmalen hat und eine Instanz benötigt, die bis auf ein anderes Merkmal identisch ist, kann eine exponierte Feldstruktur dies viel schneller erreichen als jeder andere Typ, und der Vorteil wächst nur, wenn Strukturen erhalten größer.
Supercat
7

Wenn die .NET System.Tuple <...> -Typen als Strukturen definiert wären, wären sie nicht skalierbar. Beispielsweise skaliert ein ternäres Tupel langer Ganzzahlen derzeit wie folgt:

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

Wenn das ternäre Tupel als Struktur definiert wäre, wäre das Ergebnis wie folgt (basierend auf einem von mir implementierten Testbeispiel):

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

Da Tupel eine integrierte Syntaxunterstützung in F # haben und in dieser Sprache extrem häufig verwendet werden, besteht für "struct" -Tupel bei F # -Programmierern das Risiko, ineffiziente Programme zu schreiben, ohne sich dessen überhaupt bewusst zu sein. Es würde so leicht passieren:

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

Meiner Meinung nach würden "Struktur" -Tupel mit hoher Wahrscheinlichkeit zu erheblichen Ineffizienzen bei der täglichen Programmierung führen. Andererseits verursachen die derzeit vorhandenen "Klassen" -Tupel auch bestimmte Ineffizienzen, wie von @Jon erwähnt. Ich denke jedoch, dass das Produkt aus "Eintrittswahrscheinlichkeit" mal "potenziellem Schaden" bei Strukturen viel höher wäre als derzeit bei Klassen. Daher ist die aktuelle Implementierung das geringere Übel.

Idealerweise gibt es sowohl "Klassen" -Tupel als auch "Struktur" -Tupel, beide mit syntaktischer Unterstützung in F #!

Bearbeiten (2017-10-07)

Strukturtupel werden jetzt wie folgt vollständig unterstützt:

Marc Sigrist
quelle
2
Wenn unnötiges Kopieren vermieden wird, ist eine exponierte Feldstruktur beliebiger Größe effizienter als eine unveränderliche Klasse derselben Größe, es sei denn, jede Instanz wird so oft kopiert, dass die Kosten für das Kopieren die Kosten für die Erstellung eines Heap-Objekts (die Die Gewinnschwelle der Kopien hängt von der Objektgröße ab. Ein solches Kopieren kann unvermeidbar sein, wenn man eine Struktur möchte, die vorgibt, unveränderlich zu sein, aber Strukturen, die als Sammlungen von Variablen erscheinen sollen (was Struktur ist ), können effizient verwendet werden, selbst wenn sie riesig sind.
Supercat
2
Es kann sein, dass F # nicht gut mit der Idee spielt, Strukturen zu übergeben ref, oder die Tatsache nicht mag, dass sogenannte "unveränderliche Strukturen" dies nicht tun, insbesondere wenn sie in einer Box enthalten sind. Es ist schade, dass .net das Konzept der Übergabe von Parametern durch einen Durchsetzbaren nie implementiert hat const ref, da in vielen Fällen eine solche Semantik wirklich erforderlich ist.
Supercat
1
Im Übrigen betrachte ich die fortgeführten Anschaffungskosten von GC als Teil der Kosten für die Zuweisung von Objekten. Wenn ein L0-GC nach jedem Megabyte Zuweisung erforderlich wäre, betragen die Kosten für die Zuweisung von 64 Byte etwa 1 / 16.000 der Kosten eines L0-GC zuzüglich eines Bruchteils der Kosten für alle L1- oder L2-GCs, die als erforderlich werden Folge davon.
Supercat
4
"Ich denke, dass das Produkt aus Eintrittswahrscheinlichkeit und potenziellem Schaden bei Strukturen viel höher wäre als derzeit bei Klassen." FWIW, ich habe sehr selten Tupel von Tupeln in freier Wildbahn gesehen und betrachte sie als Designfehler, aber ich sehe sehr oft Leute, die mit schrecklicher Leistung zu kämpfen haben, wenn sie (ref) Tupel als Schlüssel in einem verwenden Dictionary, z. B. hier: stackoverflow.com/questions/5850243 /…
JD
3
@ Jon Es ist zwei Jahre her, seit ich diese Antwort geschrieben habe, und ich stimme Ihnen jetzt zu, dass es vorzuziehen wäre, wenn mindestens 2- und 3-Tupel Strukturen wären. Diesbezüglich wurde ein Vorschlag für eine Benutzerstimme in F # -Sprache gemacht. Das Problem hat eine gewisse Dringlichkeit, da die Anwendungen in den Bereichen Big Data, quantitative Finanzen und Spiele in den letzten Jahren massiv zugenommen haben.
Marc Sigrist
4

Für 2-Tupel können Sie immer noch das KeyValuePair <TKey, TValue> aus früheren Versionen des Common Type Systems verwenden. Es ist ein Werttyp.

Eine kleine Klarstellung des Artikels von Matt Ellis wäre, dass der Unterschied in der Verwendungssemantik zwischen Referenz- und Werttypen nur "geringfügig" ist, wenn die Unveränderlichkeit wirksam ist (was hier natürlich der Fall wäre). Dennoch denke ich, dass es im BCL-Design am besten gewesen wäre, nicht die Verwirrung darüber einzuführen, dass Tuple an einem bestimmten Schwellenwert auf einen Referenztyp übergeht.

Glenn Slayden
quelle
Wenn ein Wert nach seiner Rückgabe einmal verwendet wird, übertrifft eine Struktur mit freiliegendem Feld jeder Größe jeden anderen Typ, vorausgesetzt, sie ist nicht so ungeheuer groß, dass sie den Stapel sprengt. Die Kosten für die Erstellung eines Klassenobjekts werden nur dann erstattet, wenn die Referenz mehrmals gemeinsam genutzt wird. Es gibt Zeiten, in denen es nützlich ist, dass ein heterogener Allzwecktyp mit fester Größe eine Klasse ist, aber es gibt andere Zeiten, in denen eine Struktur besser wäre - selbst für "große" Dinge.
Supercat
Vielen Dank, dass Sie diese nützliche Faustregel hinzugefügt haben. Ich hoffe jedoch, dass Sie meine Position nicht falsch verstanden haben: Ich bin ein Junkie vom Typ Wert. ( stackoverflow.com/a/14277068 sollte keinen Zweifel lassen).
Glenn Slayden
Werttypen sind eine der großartigen Funktionen von .net, aber leider hat die Person, die den msdn dox geschrieben hat, nicht erkannt, dass es mehrere disjunkte Anwendungsfälle für sie gibt und dass unterschiedliche Anwendungsfälle unterschiedliche Richtlinien haben sollten. Der von msdn empfohlene Strukturstil sollte nur mit Strukturen verwendet werden, die einen homogenen Wert darstellen. Wenn jedoch einige unabhängige Werte dargestellt werden müssen, die mit Klebeband befestigt sind, sollte dieser Strukturstil nicht verwendet werden - man sollte eine Struktur mit verwenden exponierte öffentliche Felder.
Supercat
0

Ich weiß nicht, aber ob Sie jemals F # -Tupel verwendet haben, sind Teil der Sprache. Wenn ich eine DLL erstellt und einen Typ von Tupeln zurückgegeben habe, wäre es schön, einen Typ zu haben, in den ich das einfügen kann. Ich vermute jetzt, dass F # Teil der Sprache ist (.Net 4). Einige Änderungen an CLR wurden vorgenommen, um einige gemeinsame Strukturen zu berücksichtigen in F #

Von 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)
Bionic Cyborg
quelle