Optimierung von Programmen für Garbage Collector

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.

Source: https://habr.com/ru/post/de453082/


All Articles