Strukturen versus Klassen

93

Ich bin dabei, 100.000 Objekte im Code zu erstellen. Sie sind klein, nur mit 2 oder 3 Eigenschaften. Ich werde sie in eine generische Liste aufnehmen und wenn sie es sind, werde ich sie schleifen und den Wert überprüfen aund möglicherweise den Wert aktualisieren b.

Ist es schneller / besser, diese Objekte als Klasse oder als Struktur zu erstellen?

BEARBEITEN

ein. Die Eigenschaften sind Werttypen (außer der Zeichenfolge, denke ich?)

b. Sie könnten (wir sind uns noch nicht sicher) eine Validierungsmethode haben

BEARBEITEN 2

Ich habe mich gefragt: Werden Objekte auf dem Heap und dem Stapel vom Garbage Collector gleichermaßen verarbeitet, oder funktioniert das anders?

Michel
quelle
2
Werden sie nur öffentliche Felder haben oder werden sie auch Methoden haben? Sind die Typen primitive Typen wie Ganzzahlen? Werden sie in einem Array oder in einer Liste <T> enthalten sein?
JeffFerguson
14
Eine Liste veränderlicher Strukturen? Achten Sie auf den Velociraptor.
Anthony Pegram
1
@ Anthony: Ich fürchte, ich vermisse den Velociraptor-Witz: -s
Michel
5
Der Velociraptor-Witz stammt von XKCD. Aber wenn Sie sich mit dem Missverständnis / Implementierungsdetail "Werttypen werden auf dem Stapel zugeordnet" befassen (ggf. löschen), dann ist es Eric Lippert, auf den Sie
Greg Beech
4
velociraptor
WernerCD

Antworten:

137

Ist es schneller , diese Objekte als Klasse oder als Struktur zu erstellen?

Sie sind die einzige Person, die die Antwort auf diese Frage bestimmen kann. Probieren Sie es in beide Richtungen aus, messen Sie eine aussagekräftige, benutzerorientierte, relevante Leistungsmetrik, und Sie werden wissen, ob die Änderung in relevanten Szenarien eine sinnvolle Auswirkung auf echte Benutzer hat.

Strukturen verbrauchen weniger Heapspeicher (weil sie kleiner und leichter zu komprimieren sind, nicht weil sie "auf dem Stapel" sind). Das Kopieren dauert jedoch länger als das Referenzkopieren. Ich weiß nicht, wie hoch Ihre Leistungsmetriken für die Speichernutzung oder die Geschwindigkeit sind. Hier gibt es einen Kompromiss und Sie sind die Person, die weiß, was es ist.

Ist es besser , diese Objekte als Klasse oder als Struktur zu erstellen?

Vielleicht Klasse, vielleicht Struktur. Als Faustregel gilt: Wenn das Objekt ist:
1. Klein
2. Logischerweise ein unveränderlicher Wert
3. Es gibt viele davon
Dann würde ich in Betracht ziehen, es zu einer Struktur zu machen. Ansonsten würde ich mich an einen Referenztyp halten.

Wenn Sie ein Feld einer Struktur mutieren müssen, ist es normalerweise besser, einen Konstruktor zu erstellen, der eine vollständig neue Struktur mit dem korrekt festgelegten Feld zurückgibt. Das ist vielleicht etwas langsamer (messen Sie es!), Aber logischerweise viel einfacher zu überlegen.

Werden Objekte auf dem Heap und dem Stapel vom Garbage Collector gleichermaßen verarbeitet?

Nein , sie sind nicht identisch, da Objekte auf dem Stapel die Wurzeln der Sammlung sind . Der Garbage Collector muss nie fragen: "Lebt das Ding auf dem Stapel?" weil die Antwort auf diese Frage immer "Ja, es ist auf dem Stapel" lautet. (Jetzt können Sie sich nicht darauf verlassen, dass ein Objekt am Leben bleibt, da der Stapel ein Implementierungsdetail ist. Der Jitter darf Optimierungen einführen, die beispielsweise den normalerweise als Stapelwert registrierten Wert registrieren, und dann ist er nie auf dem Stapel Der GC weiß also nicht, dass er noch lebt. Bei einem registrierten Objekt können seine Nachkommen aggressiv gesammelt werden, sobald das Register, das sich daran festhält, nicht erneut gelesen wird.)

Der Garbage Collector muss jedoch Objekte auf dem Stapel als lebendig behandeln, genauso wie er jedes Objekt, von dem bekannt ist, dass es lebt, als lebendig behandelt. Das Objekt auf dem Stapel kann sich auf Heap-zugewiesene Objekte beziehen, die am Leben erhalten werden müssen. Daher muss der GC Stapelobjekte wie lebende Heap-zugewiesene Objekte behandeln, um die Live-Menge zu bestimmen. Aber offensichtlich werden sie nicht als "lebende Objekte" behandelt, um den Heap zu komprimieren, da sie sich überhaupt nicht auf dem Heap befinden.

Ist das klar?

Eric Lippert
quelle
Eric, wissen Sie, ob entweder der Compiler oder der Jitter die Unveränderlichkeit nutzt (möglicherweise, wenn dies erzwungen wird readonly), um Optimierungen zu ermöglichen. Ich würde nicht zulassen, dass sich dies auf die Wahl der Veränderlichkeit auswirkt (ich bin theoretisch eine Nuss für Effizienzdetails, aber in der Praxis besteht mein erster Schritt in Richtung Effizienz immer darin, eine möglichst einfache Garantie für die Korrektheit zu haben, die ich kann und daher nicht muss Verschwenden Sie CPU-Zyklen und Gehirnzyklen bei Überprüfungen und Randfällen, und es hilft dort, angemessen veränderlich oder unveränderlich zu sein. Dies würde jedoch jeder Reaktion auf Ihre Aussage entgegenwirken, dass Unveränderlichkeit langsamer sein kann.
Jon Hanna
@ Jon: Das C # Compiler optimiert const Daten aber nicht nur lesbar Daten. Ich weiß nicht, ob der JIT-Compiler Caching-Optimierungen für schreibgeschützte Felder durchführt.
Eric Lippert
Schade, wie ich weiß, dass das Wissen über Unveränderlichkeit einige Optimierungen zulässt, aber an diesem Punkt an die Grenzen meines theoretischen Wissens stößt, aber es sind Grenzen, die ich gerne ausdehnen würde. In der Zwischenzeit "es kann in beide Richtungen schneller sein, hier ist der Grund, jetzt testen und herausfinden, was in diesem Fall zutrifft" ist nützlich, um sagen zu können :)
Jon Hanna
Ich würde empfehlen, simple-talk.com/dotnet/.net-framework/… und Ihren eigenen Artikel (@Eric) zu lesen : blogs.msdn.com/b/ericlippert/archive/2010/09/30/… , um mit dem Tauchen zu beginnen in Details. Es gibt viele andere gute Artikel. Übrigens ist der Unterschied bei der Verarbeitung von 100 000 kleinen In-Memory-Objekten durch einen gewissen Speicheraufwand (~ 2,3 MB) für die Klasse kaum spürbar. Es kann leicht durch einfachen Test überprüft werden.
Nick Martyshchenko
Ja, das ist klar. Vielen Dank für Ihre umfassende Antwort (umfangreich ist besser? Google Übersetzer hat 2 Übersetzungen geliefert. Ich wollte damit sagen, dass Sie sich die Zeit genommen haben, keine kurze Antwort zu schreiben, sondern sich auch die Zeit genommen haben, alle Details zu schreiben.
Michel
23

Manchmal müssen structSie den new () -Konstruktor nicht aufrufen und die Felder direkt zuweisen, wodurch er viel schneller als gewöhnlich wird.

Beispiel:

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

ist etwa 2 bis 3 mal schneller als

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

wo Valueist ein structmit zwei Feldern ( idund isValid).

struct Value
{
    int id;
    bool isValid;

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

Auf der anderen Seite müssen die Elemente verschoben oder ausgewählte Werttypen ausgewählt werden, was Sie durch das Kopieren verlangsamen wird. Um die genaue Antwort zu erhalten, müssen Sie vermutlich Ihren Code profilieren und testen.

John Alexiou
quelle
Offensichtlich wird es viel schneller, wenn Sie Werte auch über die nativen Grenzen hinweg sammeln.
Leppie
Ich würde vorschlagen, einen anderen Namen als zu verwenden list, da der angegebene Code nicht mit a funktioniert List<Value>.
Supercat
7

Strukturen scheinen Klassen ähnlich zu sein, aber es gibt wichtige Unterschiede, die Sie beachten sollten. Zuallererst sind Klassen Referenztypen und Strukturen Werttypen. Mithilfe von Strukturen können Sie Objekte erstellen, die sich wie die integrierten Typen verhalten und deren Vorteile ebenfalls nutzen.

Wenn Sie den Operator New für eine Klasse aufrufen, wird er auf dem Heap zugewiesen. Wenn Sie jedoch eine Struktur instanziieren, wird sie auf dem Stapel erstellt. Dies führt zu Leistungssteigerungen. Außerdem werden Sie nicht wie bei Klassen mit Verweisen auf eine Instanz einer Struktur umgehen. Sie arbeiten direkt mit der struct-Instanz. Wenn Sie eine Struktur an eine Methode übergeben, wird sie daher nicht als Referenz, sondern als Wert übergeben.

Mehr hier:

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

kyndigs
quelle
4
Ich weiß, dass es auf MSDN steht, aber MSDN erzählt nicht die ganze Geschichte. Stack vs. Heap ist ein Implementierungsdetail und Strukturen werden nicht immer auf dem Stack gespeichert . Nur einen aktuellen Blog dazu finden Sie unter: blogs.msdn.com/b/ericlippert/archive/2010/09/30/…
Anthony Pegram
"... es wird als Wert übergeben ..." Sowohl Referenzen als auch Strukturen werden als Wert übergeben (es sei denn, man verwendet 'ref') - es ist unterschiedlich, ob ein Wert oder eine Referenz übergeben wird, dh Strukturen werden Wert für Wert übergeben Klassenobjekte werden Referenz für Wert übergeben, und ref-markierte Parameter übergeben Referenz für Referenz.
Paul Ruane
10
Dieser Artikel ist in mehreren wichtigen Punkten irreführend, und ich habe das MSDN-Team gebeten, ihn zu überarbeiten oder zu löschen.
Eric Lippert
2
@supercat: Um Ihren ersten Punkt anzusprechen: Der größere Punkt ist, dass in verwaltetem Code, in dem ein Wert oder ein Verweis auf einen Wert gespeichert ist, weitgehend irrelevant ist . Wir haben hart daran gearbeitet, ein Speichermodell zu erstellen, mit dem Entwickler die Laufzeit meistens in die Lage versetzen können, intelligente Speicherentscheidungen in ihrem Namen zu treffen. Diese Unterscheidungen sind sehr wichtig, wenn das Nichtverstehen wie in C schwerwiegende Folgen hat. nicht so sehr in C #.
Eric Lippert
1
@supercat: Um Ihren zweiten Punkt anzusprechen, sind keine veränderlichen Strukturen meistens böse. Zum Beispiel void M () {S s = new S (); s.Blah (); N (s); }. Refactor to: void DoBlah (S s) {s.Blah (); } void M (S s = new S (); DoBlah (s); N (s);}. Das hat gerade einen Fehler eingeführt, weil S eine veränderbare Struktur ist. Hast du den Fehler sofort gesehen? Oder hat die Tatsache, dass S ist eine veränderbare Struktur den Fehler vor Ihnen verstecken ?
Eric Lippert
6

Arrays von Strukturen werden auf dem Heap in einem zusammenhängenden Speicherblock dargestellt, während ein Array von Objekten als zusammenhängender Referenzblock mit den tatsächlichen Objekten selbst an anderer Stelle auf dem Heap dargestellt wird, wodurch Speicher sowohl für die Objekte als auch für ihre Array-Referenzen erforderlich ist .

In diesem Fall wäre es effizienter, Strukturen zu verwenden , wenn Sie sie in a platzieren List<>(und a List<>wird auf ein Array gesichert).

(Beachten Sie jedoch, dass große Arrays ihren Weg auf dem Heap für große Objekte finden, wo sich bei langer Lebensdauer die Speicherverwaltung Ihres Prozesses nachteilig auswirken kann. Denken Sie auch daran, dass der Speicher nicht die einzige Überlegung ist.)

Paul Ruane
quelle
Sie können das refSchlüsselwort verwenden, um damit umzugehen.
Leppie
"Beachten Sie jedoch, dass große Arrays ihren Weg auf dem Heap für große Objekte finden, wo sich bei langer Lebensdauer die Speicherverwaltung Ihres Prozesses nachteilig auswirken kann." - Ich bin mir nicht ganz sicher, warum du das denkst? Die Zuweisung auf dem LOH hat keine nachteiligen Auswirkungen auf die Speicherverwaltung, es sei denn, es handelt sich (möglicherweise) um ein kurzlebiges Objekt, und Sie möchten den Speicher schnell zurückgewinnen, ohne auf eine Gen 2-Sammlung zu warten.
Jon Artus
@ Jon Artus: Der LOH wird nicht verdichtet. Jedes langlebige Objekt teilt das LOH in den Bereich des freien Speichers vor und den Bereich nach. Für die Zuweisung ist zusammenhängender Speicher erforderlich. Wenn diese Bereiche für eine Zuweisung nicht groß genug sind, wird dem LOH mehr Speicher zugewiesen (dh Sie erhalten eine LOH-Fragmentierung).
Paul Ruane
4

Wenn sie eine Wertesemantik haben, sollten Sie wahrscheinlich eine Struktur verwenden. Wenn sie Referenzsemantik haben, sollten Sie wahrscheinlich eine Klasse verwenden. Es gibt Ausnahmen, die meistens dazu neigen, eine Klasse zu erstellen, selbst wenn es eine Wertsemantik gibt, aber von dort aus beginnen.

Bei Ihrer zweiten Bearbeitung befasst sich der GC nur mit dem Heap, aber es gibt viel mehr Heap-Speicherplatz als Stapelspeicher, sodass das Platzieren von Dingen auf dem Stapel nicht immer ein Gewinn ist. Außerdem befinden sich in beiden Fällen eine Liste der Strukturtypen und eine Liste der Klassentypen auf dem Heap, sodass dies in diesem Fall irrelevant ist.

Bearbeiten:

Ich fange an, den Begriff böse als schädlich zu betrachten. Schließlich ist es eine schlechte Idee, eine Klasse veränderbar zu machen, wenn sie nicht aktiv benötigt wird, und ich würde nicht ausschließen, jemals eine veränderbare Struktur zu verwenden. Es ist eine schlechte Idee, so oft, dass es fast immer eine schlechte Idee ist, aber meistens stimmt sie einfach nicht mit der Wertesemantik überein, so dass es im gegebenen Fall einfach keinen Sinn macht, eine Struktur zu verwenden.

Bei privaten verschachtelten Strukturen kann es vernünftige Ausnahmen geben, bei denen alle Verwendungen dieser Struktur daher auf einen sehr begrenzten Bereich beschränkt sind. Dies gilt hier jedoch nicht.

Wirklich, ich denke, "es mutiert, also ist es ein schlechtes Produkt" ist nicht viel besser, als über den Heap und den Stack zu sprechen (was zumindest einige Auswirkungen auf die Leistung hat, selbst wenn es sich um ein häufig falsch dargestelltes handelt). "Es mutiert, daher ist es sehr wahrscheinlich nicht sinnvoll, es als Wertsemantik zu betrachten, also ist es eine schlechte Struktur" ist nur geringfügig anders, aber wichtig, denke ich.

Jon Hanna
quelle
3

Die beste Lösung besteht darin, zu messen, erneut zu messen und dann noch etwas zu messen. Möglicherweise gibt es Details zu Ihrer Arbeit, die eine vereinfachte und einfache Antwort wie "Strukturen verwenden" oder "Klassen verwenden" erschweren.

FMM
quelle
Ich stimme dem Maßnahmesteil zu, aber meiner Meinung nach war es ein direktes und klares Beispiel, und ich dachte, dass vielleicht einige allgemeine Dinge darüber gesagt werden könnten. Und wie sich herausstellte, taten es einige Leute.
Michel
3

Eine Struktur ist im Kern nichts anderes als eine Ansammlung von Feldern. In .NET kann eine Struktur "vorgeben", ein Objekt zu sein, und für jeden Strukturtyp definiert .NET implizit einen Heap-Objekttyp mit denselben Feldern und Methoden, die sich als Heap-Objekt wie ein Objekt verhalten . Eine Variable, die einen Verweis auf ein solches Heap-Objekt enthält ("Boxed" -Struktur), weist eine Referenzsemantik auf, aber eine Variable, die eine Struktur direkt enthält, ist einfach eine Aggregation von Variablen.

Ich denke, ein Großteil der Verwirrung zwischen Struktur und Klasse beruht auf der Tatsache, dass Strukturen zwei sehr unterschiedliche Anwendungsfälle haben, die sehr unterschiedliche Entwurfsrichtlinien haben sollten, aber die MS-Richtlinien unterscheiden nicht zwischen ihnen. Manchmal besteht Bedarf an etwas, das sich wie ein Objekt verhält. In diesem Fall sind die MS-Richtlinien ziemlich vernünftig, obwohl das "16-Byte-Limit" wahrscheinlich eher 24-32 betragen sollte. Manchmal ist jedoch eine Aggregation von Variablen erforderlich. Eine zu diesem Zweck verwendete Struktur sollte einfach aus einer Reihe öffentlicher Felder und möglicherweise aus einem EqualsOverride, ToStringOverride und bestehenIEquatable(itsType).EqualsImplementierung. Strukturen, die als Aggregationen von Feldern verwendet werden, sind keine Objekte und sollten dies auch nicht vorgeben. Aus Sicht der Struktur sollte die Bedeutung des Feldes nicht mehr oder weniger sein als "das Letzte, was in dieses Feld geschrieben wurde". Jede zusätzliche Bedeutung sollte durch den Client-Code bestimmt werden.

Wenn beispielsweise eine variabel aggregierende Struktur Mitglieder hat Minimumund Maximum, sollte die Struktur selbst dies nicht versprechen Minimum <= Maximum. Code, der eine solche Struktur als Parameter empfängt, sollte sich so verhalten, als ob er separat Minimumund mit MaximumWerten übergeben würde. Eine Anforderung, Minimumdie nicht größer als Maximumist, sollte als Anforderung angesehen werden, dass ein MinimumParameter nicht größer als ein separat übergebener Parameter sein darf Maximum.

Ein nützliches Muster, das manchmal berücksichtigt werden muss, ist die ExposedHolder<T>Definition einer Klasse wie:

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

Wenn man eine Struktur hat List<ExposedHolder<someStruct>>, bei der someStructes sich um eine variabel aggregierende Struktur handelt, kann man Dinge wie tun myList[3].Value.someField += 7;, aber wenn man myList[3].Valueanderen Code gibt, erhält man den Inhalt von, Valueanstatt ihm ein Mittel zu geben, ihn zu ändern. Im Gegensatz List<someStruct>dazu wäre es notwendig , wenn man a verwendet , es zu verwenden var temp=myList[3]; temp.someField += 7; myList[3] = temp;. Wenn ein veränderbarer Klassentyp verwendet wird, myList[3]müssen alle Felder in ein anderes Objekt kopiert werden , um den Inhalt für externen Code verfügbar zu machen. Wenn man einen unveränderlichen Klassentyp oder eine Struktur im "Objektstil" verwendet, muss man eine neue Instanz erstellen, die ähnlich ist, myList[3]außer dass sie someFieldanders ist, und diese neue Instanz dann in der Liste speichern.

Ein zusätzlicher Hinweis: Wenn Sie eine große Anzahl ähnlicher Dinge speichern, kann es sinnvoll sein, sie in möglicherweise verschachtelten Arrays von Strukturen zu speichern, wobei Sie vorzugsweise versuchen, die Größe jedes Arrays zwischen 1 KB und 64 KB oder so zu halten. Arrays von Strukturen sind insofern besonders, als die Indizierung einen direkten Verweis auf eine Struktur innerhalb ergibt, so dass man "a [12] .x = 5;" sagen kann. Obwohl man Array-ähnliche Objekte definieren kann, erlaubt C # ihnen nicht, diese Syntax mit Arrays zu teilen.

Superkatze
quelle
1

Verwenden Sie Klassen.

Im Allgemeinen. Warum nicht den Wert b aktualisieren, während Sie sie erstellen?

Preet Sangha
quelle
1

Aus C ++ - Sicht stimme ich zu, dass das Ändern von Struktureigenschaften im Vergleich zu einer Klasse langsamer sein wird. Ich denke jedoch, dass sie schneller zu lesen sind, da die Struktur auf dem Stapel anstelle des Heaps zugewiesen wird. Das Lesen von Daten vom Heap erfordert mehr Überprüfungen als vom Stapel.

Robert
quelle
1

Wenn Sie sich schließlich für struct entscheiden, entfernen Sie den String und verwenden Sie einen Char- oder Byte-Puffer mit fester Größe.

Das ist re: Leistung.

Daniel Mošmondor
quelle