Vor nicht allzu langer Zeit erschien ein ausgezeichneter Artikel über die Habré- Optimierung der Speicherbereinigung in einem hoch geladenen .NET-Dienst . Dieser Artikel ist insofern sehr interessant, als die mit der Theorie bewaffneten Autoren früher das Unmögliche getan haben: Sie haben ihre Anwendung mithilfe des GC-Wissens optimiert. Und wenn wir früher keine Ahnung hatten, wie dieser GC funktioniert, wird er uns jetzt auf einem Silbertablett durch die Bemühungen von Konrad Cocos in seinem Buch Pro .NET Memory Management präsentiert . Welche Schlussfolgerungen habe ich für mich gezogen? Lassen Sie uns eine Liste von Problembereichen erstellen und überlegen, wie diese gelöst werden können.
Beim letzten CLRium # 5-Workshop: Garbage Collector haben wir den ganzen Tag über GC gesprochen. Ich habe mich jedoch entschlossen, einen Bericht mit Textdecodierung zu veröffentlichen. Dies ist ein Vortrag über Schlussfolgerungen zur Anwendungsoptimierung.
Reduzieren Sie die generationenübergreifende Konnektivität
Das Problem
Um die Geschwindigkeit der Speicherbereinigung zu optimieren, erfasst der GC nach Möglichkeit die jüngere Generation. Dazu benötigt er aber auch Informationen zu Links älterer Generationen (in diesem Fall als zusätzliche Wurzel): der Kartentabelle.
Gleichzeitig zwingt Sie eine Verbindung von der älteren zur jüngeren Generation, den Bereich mit einem Kartentisch abzudecken:
- 4 Bytes überlappen 4 kb oder max. 320 Objekte - für x86-Architektur
- 8 Bytes überlappen 8 kb oder max. 320 Objekte - für x64-Architektur
Das heißt, GC, der die Kartentabelle überprüft und einen Wert ungleich Null erreicht, muss maximal 320 Objekte auf das Vorhandensein ausgehender Links in unserer Generation überprüfen.
Daher werden spärliche Verbindungen in der jüngeren Generation die GC zeitaufwändiger machen
Lösung
- Suchen Sie Objekte mit Verbindungen in der jüngeren Generation - in der Nähe;
- Wenn der Verkehr von Objekten der Generation Null angenommen wird, verwenden Sie das Ziehen. Das heißt, Erstellen Sie einen Pool von Objekten (es wird keine neuen geben: Es wird keine Objekte der Nullgenerierung geben). Wenn Sie den Pool mit zwei aufeinander folgenden GCs "aufwärmen", sodass der Inhalt in der zweiten Generation garantiert fehlschlägt, vermeiden Sie außerdem Links zur jüngeren Generation und haben Nullen in der Kartentabelle.
- Vermeiden Sie Verbindungen zur jüngeren Generation.
Vermeiden Sie starke Konnektivität
Das Problem
Wie folgt aus den Algorithmen der Komprimierungsphase von Objekten in SOH:
- Um den Heap zu komprimieren, müssen Sie den Baum umrunden und alle Links überprüfen und auf neue Werte korrigieren
- Darüber hinaus wirken sich Links aus der Kartentabelle auf ganze Objektgruppen aus
Daher kann die allgemein starke Konnektivität von Objekten während der GC zu einem Absinken führen.
Lösung
- Haben stark verbundene Objekte in der Nähe, in einer Generation
- Vermeiden Sie im Allgemeinen unnötige Links (verwenden Sie beispielsweise das bereits vorhandene this-> Service-> Handle, anstatt this-> handle-Links zu duplizieren).
- Vermeiden Sie Code mit versteckter Konnektivität. Zum Beispiel Verschlüsse
Überwachen Sie die Segmentnutzung
Das Problem
Während intensiver Arbeit kann es zu einer Situation kommen, in der die Zuweisung neuer Objekte zu Verzögerungen führt: die Zuweisung neuer Segmente unter dem Haufen und deren weitere Aufhebung bei der Müllreinigung
Lösung
- Verwenden von PerfMon / Sysinternal Utilities zur Steuerung der Auswahlpunkte neuer Segmente sowie deren Aufhebung und Freigabe
- Wenn es sich um LOH handelt, bei dem es sich um einen dichten Pufferverkehr handelt, verwenden Sie ArrayPool
- Wenn es sich um SOH handelt, stellen Sie sicher, dass Objekte derselben Lebensdauer in der Nähe hervorgehoben sind und Sweep anstelle von Collect bereitstellen
- SOH: Objektpools verwenden
Ordnen Sie keinen Speicher in geladenen Codeabschnitten zu
Das Problem
Der geladene Codeabschnitt weist Speicher zu:
- Infolgedessen wählt GC ein Zuordnungsfenster von nicht 1 KB, sondern 8 KB aus.
- Wenn das Fenster keinen Platz mehr hat, führt dies zu einer GC und einer Erweiterung der geschlossenen Zone
- Ein dichter Strom neuer Objekte führt dazu, dass kurzlebige Objekte aus anderen Threads mit schlechteren Bedingungen für die Speicherbereinigung schnell an die ältere Generation weitergeleitet werden
- Dies erhöht die Speicherbereinigungszeit
- Dies führt auch im gleichzeitigen Modus zu einem längeren Stopp der Welt
Lösung
- Vollständiges Verbot der Verwendung von Verschlüssen in kritischen Codeabschnitten
- Vollständiges Verbot des Boxens in kritischen Abschnitten des Codes (Sie können die Emulation bei Bedarf durch Ziehen verwenden)
- Verwenden Sie Strukturen, wenn ein temporäres Objekt für die Datenspeicherung erstellt werden muss. Besser ist ref struct. Wenn die Anzahl der Felder mehr als 2 beträgt, senden Sie mit ref
Vermeiden Sie unnötige Speicherzuordnungen in LOH
Das Problem
Das Platzieren von Arrays in LOH führt entweder zur Fragmentierung oder zur Gewichtung des GC-Verfahrens
Lösung
- Verwenden Sie die Aufteilung von Arrays in Subarrays und eine Klasse, die die Logik der Arbeit mit solchen Arrays kapselt (dh anstelle von List <T>, in der das Mega-Array gespeichert ist, die MyList mit Array [] [], wodurch das Array etwas kürzer aufgeteilt wird).
- Arrays gehen an SOH
- Nach ein paar Müllsammlungen legen sie sich neben immer lebende Objekte und hören auf, die Müllsammlung zu beeinflussen
- Steuern Sie die Verwendung von Doppelarrays mit einer Länge von mehr als 1000 Elementen.
Verwenden Sie, wo dies gerechtfertigt und möglich ist, einen Fadenstapel
Das Problem
Es gibt eine Reihe von ultrakurzen Objekten oder Objekten, die in einem Methodenaufruf leben (einschließlich interner Aufrufe). Sie erzeugen Objektverkehr
Lösung
- Verwenden der Speicherzuordnung auf dem Stapel, wo dies möglich ist:
- Es wird kein Haufen geladen
- Lädt GC nicht
- Speicher freigeben - Sofort
- Verwenden Sie
Span T x = stackalloc T[];
anstelle von new T[]
wo möglich - Verwenden Sie nach Möglichkeit
Span/Memory
- Konvertieren Sie Algorithmen in
ref stack
Typen (StackList: struct, ValueStringBuilder )
Freie Objekte so früh wie möglich
Das Problem
Als kurzlebig konzipiert, fallen Objekte in Gen1 und manchmal in Gen2.
Dies führt zu einer schwereren GC, die länger anhält
Lösung
- Sie müssen die Objektreferenz so früh wie möglich freigeben
- Wenn ein langer Algorithmus Code enthält, der mit Objekten funktioniert, wird er durch Code voneinander getrennt. Da diese jedoch an einem Ort gruppiert werden können, muss sie gruppiert werden, damit sie früher gesammelt werden können.
- In Zeile 10 wurde beispielsweise die Sammlung herausgenommen und in Zeile 120 herausgefiltert.
Sie müssen GC.Collect () nicht aufrufen.
Das Problem
Wenn Sie GC.Collect () aufrufen, wird die Situation häufig behoben
Lösung
- Es ist viel korrekter, die GC-Betriebsalgorithmen zu erlernen, die Anwendung unter ETW und andere Diagnosetools (JetBrains dotMemory, ...) zu betrachten.
- Optimieren Sie die problematischsten Bereiche
Vermeiden Sie das Feststecken
Das Problem
Das Fixieren wirft eine Reihe von Problemen auf:
- Kompliziert die Speicherbereinigung
- Erstellt freie Speicherplätze (Knoten mit freien Listenelementen, Steintabelle, Buckets)
- Kann einige Objekte in der jüngeren Generation belassen, während Links vom Kartentisch gebildet werden
Lösung
Wenn es keinen anderen Ausweg gibt, verwenden Sie fixed () {}. Diese Festschreibungsmethode führt keine echte Festschreibung durch: Sie tritt nur auf, wenn der GC in geschweiften Klammern gearbeitet hat.
Vermeiden Sie die Finalisierung
Das Problem
Die Finalisierung wird nicht deterministisch genannt:
- Uninvited Dispose () führt zur Finalisierung mit allen ausgehenden Links vom Objekt
- Abhängige Objekte verzögern sich länger als geplant
- Altern, Übergang zu älteren Generationen
- Wenn sie gleichzeitig Links zu jüngeren enthalten, generieren sie Links aus der Kartentabelle
- Kompliziert die Montage älterer Generationen, fragmentiert sie und führt zu Verdichtung statt Sweep
Lösung
Rufen Sie vorsichtig Dispose () auf
Vermeiden Sie zu viele Fäden
Das Problem
Mit einer großen Anzahl von Threads wächst der Zuordnungskontext als Sie sind jedem Thread zugeordnet:
- Dadurch wird GC.Collect schneller.
- Aufgrund des Platzmangels im kurzlebigen Segment folgt Collect dem Collective Sweep
Lösung
- Steuern Sie die Anzahl der Threads anhand der Anzahl der Kerne
Vermeiden Sie den Verkehr von Objekten unterschiedlicher Größe
Das Problem
Beim Verkehr mit Objekten unterschiedlicher Größe und Lebensdauer tritt eine Fragmentierung auf:
- Erhöhen Sie das Fragmentierungsverhältnis
- Sammlungsauslösung mit einer Adressänderungsphase in allen referenzierenden Objekten
Lösung
Wenn der Verkehr von Objekten angenommen wird:
- Überprüfen Sie, ob zusätzliche Felder vorhanden sind, die ungefähr der Größe entsprechen
- Überprüfen Sie, ob keine Zeichenfolgenmanipulation vorhanden ist: Ersetzen Sie diese nach Möglichkeit durch ReadOnlySpan / ReadOnlyMemory
- Link so schnell wie möglich freigeben
- Nutzen Sie das Ziehen
- Warme Caches und Pools mit einem doppelten GC, um Objekte zu verdichten. So vermeiden Sie Probleme mit dem Kartentisch.