Kann ich den CLR GC "primieren", um mit verschwenderischem Speicherverbrauch zu rechnen?

8

Wir haben eine Server-App, die viele Speicherzuweisungen (sowohl kurzlebige als auch langlebige) durchführt. Wir sehen eine Menge GC2-Sammlungen kurz nach dem Start, aber diese Sammlungen beruhigen sich nach einer gewissen Zeit (obwohl das Speicherzuweisungsmuster konstant ist). Diese Kollektionen erreichen schon früh ihre Leistung.

Ich vermute, dass dies durch GC-Budgets verursacht werden könnte (für Gen2?). Gibt es eine Möglichkeit, dieses Budget (direkt oder indirekt) so einzustellen, dass mein Server am Anfang besser abschneidet?

Eine kontraintuitive Reihe von Ergebnissen, die ich gesehen habe: Wir haben die Menge an Speicher (und Large Object Heap) Allokationen stark reduziert, wodurch sich die Performance auf lange Sicht verbessert hat, aber die frühe Performance wird schlechter und die Die "Beruhigungsphase" wird länger.

Der GC braucht offenbar eine gewisse Zeit, um zu realisieren, dass unsere App ein Speicher ist und passt sich dementsprechend an. Ich kenne diese Tatsache bereits, wie überzeuge ich den GC?

Bearbeiten

  • Betriebssystem: 64-Bit Windows Server 2008 R2
  • Wir verwenden .Net 4.0 ServerGC Batch Latency. Versucht 4,5 und die 3 verschiedenen Latenzmodi, und während die durchschnittliche Leistung leicht verbessert wurde, verschlechterte sich die Worst-Case-Leistung tatsächlich

Bearbeiten2

  • Ein GC-Spike kann die doppelte Zeit (wir sprechen von Sekunden) von "akzeptabel" auf "inakzeptabel" umstellen
  • Fast alle Spikes korrelieren mit Gen 2-Sammlungen
  • Mein Testlauf verursacht eine endgültige 32-GB-Heap-Größe. Die anfängliche Schaumbildung dauert für das 1. 1/5 der Laufzeit, und die Leistung danach ist tatsächlich besser (weniger häufige Spitzen), obwohl der Haufen wächst. Der letzte Spike am Ende des Tests (mit der größten Heap-Größe) ist die gleiche Höhe (d. H. So schlecht wie) 2 der Spikes in der anfänglichen "Trainings" -Periode (mit viel kleineren Heaps)
Rob 26.11.2013, 10:57
quelle

1 Antwort

6

Die Zuweisung extrem großer Heaps in .NET kann wahnsinnig schnell sein, und die Anzahl der blockierenden Sammlungen verhindert nicht, dass sie so schnell ist. Probleme, die Sie beobachten, werden durch die Tatsache verursacht, dass Sie nicht nur allokieren, sondern auch Code haben, der die Abhängigkeitsreorganisation und die eigentliche Garbage-Collection zur gleichen Zeit verursacht, wenn die Zuweisung stattfindet.

Es gibt ein paar Techniken zu beachten:

  • Versuchen Sie es mit LatencyMode ( Ссылка) ), setzen Sie sie auf LowLatency, während Sie die Daten aktiv laden - siehe auch die Kommentare zu dieser Antwort

  • verwenden mehrere Threads

  • keine Querverweise auf neu zugewiesene Objekte während des aktiven Ladevorgangs ausfüllen; zuerst die aktive Zuordnungsphase durchlaufen, nur Querverweise für Elemente verwenden, jedoch keine verwalteten Referenzen; Dann erzwinge GC-Paar-Zeiten, um alles in Gen2 zu haben, und befülle erst dann deine erweiterten Datenstrukturen; Möglicherweise müssen Sie Ihre Deserialisierungslogik überdenken, um dies zu ermöglichen

  • versuchen Sie, Ihre größten Root-Sammlungen (Arrays von Objekten, Strings) so früh wie möglich in die zweite Generation zu zwingen; Tun Sie dies, indem Sie sie vorallokieren und den vollständigen GC zweimal erzwingen, bevor Sie damit beginnen, Daten zu füllen (Millionen von kleinen Objekten laden); Wenn Sie eine Variante des generischen Dictionary verwenden, stellen Sie sicher, dass Sie die Kapazität frühzeitig zuweisen, um Reorganisationen zu vermeiden.

  • Jedes große Array von Referenzen ist eine große Quelle für GC-Overhead - bis sowohl Array- als auch referenzierte Objekte in Gen2 sind; je größer das Array, desto größer der Overhead; bevorzugen Arrays von Indizes zu Arrays von Referenzen, insbesondere für temporäre Verarbeitung Bedürfnisse

  • Vermeiden Sie, dass viele Utilities oder temporäre Objekte freigegeben oder heraufgestuft werden während Sie in der aktiven Ladephase eines Threads sind, schauen Sie sich Ihren Code für String-Verkettung, Boxing und 'foreach' Iteratoren an t wird automatisch in 'for' Loops

  • optimiert
  • Wenn Sie ein Array von Referenzen und eine Hierarchie von Funktionsaufrufen haben, die einige lang andauernde enge Schleifen haben, vermeiden Sie die Einführung lokaler Variablen, die den Referenzwert von einer Position im Array zwischenspeichern; Stattdessen sollten Sie den Offset-Wert zwischenspeichern und etwas wie das Konstrukt "myArrayOfObjects [offset]" auf allen Ebenen Ihrer Funktionsaufrufe verwenden. Es hat mir sehr dabei geholfen, vorbelegte, große Gen2-Datenstrukturen zu verarbeiten. Meine persönliche Theorie ist, dass dies GC hilft, temporäre Abhängigkeiten von den Datenstrukturen Ihres lokalen Threads zu verwalten und so die Parallelität zu verbessern.

Hier sind die Gründe für dieses Verhalten, soweit ich durch das Auffüllen von ~ 100 GB RAM während des App-Starts gelernt habe, mit mehreren Threads:

  • Wenn GC Daten von einer Generation in eine andere verschiebt, kopiert sie sie und modifiziert somit alle Referenzen; Je weniger Querverweise Sie während der aktiven Ladephase haben, desto besser

  • GC verwaltet viele interne Datenstrukturen, die Referenzen verwalten; Wenn Sie die Referenzen selbst massiv ändern - oder wenn Sie viele Referenzen haben, die während der GC geändert werden müssen - verursacht dies sowohl bei der Blockierung als auch bei der gleichzeitigen GC einen erheblichen Overhead der CPU- und Speicherbandbreite. Manchmal beobachtete ich, dass GC 30-80% der CPU konsumierte, ohne dass irgendwelche Sammlungen stattfanden - einfach durch eine gewisse Verarbeitung, die merkwürdig aussieht, bis man merkt, dass jedes Mal, wenn man eine Referenz auf ein Array oder eine temporäre Variable in einer engen Schleife platziert, GC muss Datenstrukturen zur Abhängigkeitsverfolgung ändern und manchmal neu organisieren

  • server GC verwendet threadspezifische Gen0-Segmente und kann das gesamte Segment auf die nächste Generation verschieben (ohne Daten tatsächlich zu kopieren - bin jedoch nicht sicher), beachte dies beim Entwerfen eines Multi-Thread-Datenladeprozesses

  • ConcurrentDictionary ist zwar eine großartige API, skaliert jedoch in extremen Szenarien mit mehreren Kernen nicht gut, wenn die Anzahl der Objekte über einige Millionen liegt (z. B. die Verwendung einer nicht verwalteten Hashtabelle für die gleichzeitige Einfügung, z TBB)

  • Wenn möglich oder anwendbar, erwägen Sie die Verwendung eines nativen Pooled Allocator (wieder Intel TBB)

BTW, das neueste Update für .NET 4.5 unterstützt Defragmentierung für große Objekte. Ein weiterer guter Grund, um darauf zu aktualisieren.

.NET 4.6 verfügt auch über eine API, die nach GC (GC.TryStartNoGCRegion) fragt, wenn bestimmte Bedingungen erfüllt sind: Ссылка

Siehe auch einen verwandten Beitrag von Maoni Stephens: Ссылка

    
Roman Polunin 26.11.2013, 21:22
quelle