Wie geht man mit der Unveränderlichkeit von zurückgegebenen Strukturen um?

8

Ich schreibe ein Spiel mit einem riesigen 2D-Array von "Zellen". Eine Zelle benötigt nur 3 Bytes. Ich habe auch eine Klasse namens CellMap, die das 2D-Array als privates Feld enthält und über einen öffentlichen Indexer darauf zugreifen kann.

Die Profilerstellung hat gezeigt, dass ein Leistungsproblem durch die Speicherbereinigung von zu vielen Zellenobjekten verursacht wird. Also entschied ich Cell zu struct zu machen (es war eine Klasse).

Aber jetzt funktioniert Code wie dieser nicht:

%Vor%

Ich kann mir viele Optionen vorstellen, aber ich mag sie nicht wirklich.

  1. Machen Sie das Array öffentlich und schreiben Sie cellMap.Data[x, y].Population = 5;
  2. Beenden Sie die Verwendung einer CellMap-Klasse und verwenden Sie direkt ein 2D-Array. Aber CellMap ist sehr praktisch, weil es seine eigene optimierte Serialisierung implementiert und die Eigenschaften Width und Height freilegt, die bequemer sind als das Schreiben von cellMap.GetLength(0) .
  3. Zelle unveränderlich machen Aber wie würde der Code aussehen? %Code%? Sehr ausführlich.
  4. Ein paar Dienstprogrammfunktionen wie cellMap[x, y] = IncrementCellPopulation(cellMap[x, y])
  5. Fügen Sie in jeder Klasse, die eine CellMap besitzt, eine Dienstprogrammeigenschaft wie cellMap.SetPopulationAt(x, y, 5) hinzu, so dass mein Code wie private Cell[,] CellData { get { return this.CellMap.GetInternalArray(); } } aussehen kann.

Wie wird dieses Problem traditionell gelöst?

    
Stefan Monov 01.09.2010, 13:43
quelle

6 Antworten

18

Es gibt also eigentlich zwei Probleme. Es gibt die Frage, die Sie tatsächlich gestellt haben: Was sind Techniken, um mit der Tatsache umzugehen, dass Strukturen unveränderlich sein sollten, weil sie nach Wert kopiert werden, aber Sie einen mutieren wollen. Und dann ist da noch die Frage, die dieses Thema motiviert: "Wie kann ich die Leistung meines Programms akzeptabel machen?"

Meine andere Antwort bezieht sich auf die erste Frage, aber die zweite Frage ist ebenfalls interessant.

Wenn der Profiler tatsächlich festgestellt hat, dass das Leistungsproblem auf die Speicherbereinigung von Zellen zurückzuführen ist, dann ist es möglich , dass das Erstellen einer Zelle in eine Struktur hilfreich ist. Es ist auch möglich, dass es überhaupt nicht hilft, und es ist möglich, dass es dadurch noch schlimmer wird.

Ihre Zellen enthalten keine Referenztypen; Wir wissen das, weil Sie gesagt haben, dass sie nur drei Bytes sind. Wenn jemand, der dies liest, denkt, dass er eine Leistungsoptimierung durchführen kann, indem er eine Klasse in eine Struktur umwandelt, hilft es vielleicht gar nicht, weil die Klasse ein Feld des Referenztyps enthält Der Garbage Collector muss immer noch jede Instanz erfassen, selbst wenn er in einen Werttyp umgewandelt wird. Die Referenztypen darin müssen ebenfalls gesammelt werden! Ich würde nur empfehlen, dies aus Leistungsgründen zu versuchen, wenn Zelle nur Werttypen enthält, was sie anscheinend tut.

Es könnte es schlimmer machen, weil Werttypen kein Allheilmittel sind; Sie haben auch Kosten. Werttypen sind oft teurer zu kopieren als Referenztypen (die fast immer die Größe eines Registers haben, fast immer an der entsprechenden Speichergrenze ausgerichtet sind, und daher ist der Chip in hohem Maße für das Kopieren optimiert). Und Werttypen werden ständig kopiert.

Nun, in Ihrem Fall haben Sie eine Struktur, die kleiner als eine Referenz ist; Referenzen sind typischerweise vier oder acht Bytes. Und Sie setzen sie in ein Array, was bedeutet, dass Sie das Array zusammenpacken; Wenn Sie tausend von ihnen haben, dauert es dreitausend Bytes. Das bedeutet, dass drei von vier Strukturen darin fehlausgerichtet sind, was mehr Zeit (auf vielen Chiparchitekturen) bedeutet, um den Wert aus dem Array zu erhalten. Sie könnten erwägen, die Auswirkung von padding Ihrer Struktur auf vier Bytes zu messen, um zu sehen, ob das einen Unterschied macht, vorausgesetzt, Sie behalten sie immer noch in einem Array, was mich zu meinem nächsten Punkt bringt. ..

Die Zellenabstraktion könnte einfach eine schlechte Abstraktion sein , um Daten über viele Zellen zu speichern . Wenn das Problem darin besteht, dass Zellen Klassen sind, Sie ein Array von Tausenden von Zellen behalten und deren Erfassung teuer ist, dann gibt es andere Lösungen als Cell zu einer Struktur zu machen. Angenommen, eine Zelle enthält zwei Bytes der Population und ein Byte Farbe. Das ist der Mechanismus von Cell, aber sicher ist das nicht die Schnittstelle , die Sie den Benutzern zugänglich machen möchten. Es gibt keinen Grund, warum Ihr Mechanismus den gleichen Typ wie die Schnittstelle verwenden muss . Daher könnten Sie Instanzen der Cell-Klasse bei Bedarf erstellen :

%Vor%

und so weiter. Stellen Sie die Zellen auf Anfrage her . Sie werden fast sofort gesammelt, wenn sie kurzlebig sind. CellMap ist eine Abstraktion , also verwenden Sie die Abstraktion, um die unordentlichen Implementierungsdetails zu verbergen.

Mit dieser Architektur haben Sie keine Garbage-Collection-Probleme, weil Sie fast keine Live-Cell-Instanzen haben, aber Sie können immer noch

sagen %Vor%

kein Problem, weil der erste Indexer ein unveränderliches Objekt erzeugt, das weiß, wie der Zustand der Karte aktualisiert wird . Die Zelle muss nicht veränderbar sein. Beachten Sie, dass die Cell-Klasse vollständig unveränderbar ist. (Heck, die Zelle könnte hier eine Struktur sein, obwohl sie natürlich in ICell umgewandelt würde, würde sie trotzdem boxen.) Es ist die Karte , die veränderbar ist, und die Zelle mutiert die Karte für der Benutzer.

    
Eric Lippert 01.09.2010 15:20
quelle
9

Wenn Sie Cell unveränderlich machen wollen - wie Sie es sollten, wenn es eine Struktur ist - dann ist es eine gute Technik, eine Factory, die eine Instanz -Methode ist, auf der Zelle zu erstellen:

%Vor%

Also wäre dein Code etwas wie

%Vor%

Aber ich denke, das ist möglicherweise eine Sackgasse; Es könnte besser sein, einfach nicht so viele Zellen in der Nähe zu haben, anstatt zu versuchen, eine Welt zu optimieren, in der es Tausende von ihnen gibt. Ich schreibe eine andere Antwort dazu.

    
Eric Lippert 01.09.2010 14:21
quelle
4

6. Verwenden Sie einen ref -Parameter in einer Methode, die den Wert ändert, rufen Sie ihn als IncrementCellPopulation(ref cellMap[x, y])

auf     
leppie 01.09.2010 13:48
quelle
2

Wenn Ihre Zellenzuordnung tatsächlich "spärlich" ist, dh wenn viele benachbarte Zellen entweder keinen Wert oder einen Standardwert haben, würde ich vorschlagen, dass Sie kein Zellenobjekt für diese Zellen erstellen. Erstellen Sie nur Objekte für Zellen, die tatsächlich einen nicht standardmäßigen Status haben. (Dies kann die Gesamtzahl der Zellen um einen signifikanten Betrag reduzieren, wodurch der Müllsammler entlastet wird.)

Bei diesem Ansatz müssen Sie natürlich eine neue Methode zum Speichern Ihrer Zellkarte finden. Sie müssten davon wegkommen, Ihre Zellen in einem Array zu speichern (da sie nicht spärlich sind) und eine andere Art von Datenstruktur annehmen, wahrscheinlich einen Baum.

Sie können beispielsweise Ihre Karte in eine Anzahl einheitlicher Regionen unterteilen, so dass Sie beliebige Zellkoordinaten in eine entsprechende Region übersetzen können. (Sie könnten jede Region nach derselben Idee in Unterregionen unterteilen.) Sie könnten dann einen Suchbaum pro Region haben, in dem die Zellkoordinaten als Schlüssel in den Baum wirken.

Ein solches Schema würde es Ihnen ermöglichen, nur die Zellen zu speichern, die Sie benötigen, und trotzdem schnell auf jede Zelle in Ihrer Karte zugreifen zu können. Wenn in den Bäumen keine Zelle an bestimmten Koordinaten gefunden wird, kann davon ausgegangen werden, dass es sich um eine Standardzelle handelt.

    
stakx 01.09.2010 15:54
quelle
1

Ergänzen Sie, was die CellMap tun soll, und erlauben Sie den Zugriff auf das eigentliche Array nur über geeignete Methoden wie IncrementPopupation(int x, int y) . Ein Array (oder irgendeine Variable) in den meisten Fällen öffentlich zu machen, ist ein ernsthafter Code-Geruch, wie es ein Array in .NET zurückgibt.

Verwenden Sie aus Leistungsgründen ein eindimensionales Array. diese sind in .NET viel schneller.

    
lbruder 01.09.2010 13:49
quelle
0

Eric Lipperts Ansatz ist gut, aber ich würde eher eine Basisklasse als eine Schnittstelle für den indirekten Accessor vorschlagen. Das folgende Programm demonstriert eine Klasse, die sich wie ein Sparse-Array von Punkten verhält. Vorausgesetzt, dass man niemals ein Objekt vom Typ PointRef (*) persistiert, sollten die Dinge wunderbar funktionieren. Sprich:

%Vor%

oder

%Vor%

erstellt beide ein temporäres pointRef-Objekt (in einem Fall pointRef.onePoint; in der anderen pointHolder.IndexedPointRef), aber die sich verbreiternden Typumwandlungen arbeiten, um eine Wertesemantik zu erhalten. Natürlich wäre es viel einfacher gewesen, wenn (1) Methoden für Werttypen als Mutatoren markiert werden könnten und (2) das Schreiben eines Feldes einer Struktur, auf die über die Eigenschaft zugegriffen wird, könnte automatisch die Eigenschaft lesen, die temporäre Struktur bearbeiten und schreiben es zurück. Der hier verwendete Ansatz funktioniert, obwohl ich leider keine Möglichkeit kenne, ihn generisch zu machen.

(*) Elemente des Typs PointRef sollten nur von Eigenschaften zurückgegeben werden und sollten niemals in einer Variablen gespeichert oder als Parameter für irgendetwas anderes als eine Setter-Eigenschaft verwendet werden, die in einen Point konvertiert wird.

%Vor%     
supercat 09.09.2010 01:59
quelle