1 Punkte von GN⁺ 2025-04-24 | 1 Kommentare | Auf WhatsApp teilen
  • Die Sprache Go hat fast kein undefiniertes Verhalten und verfügt über eine einfache GC-(Garbage-Collection-)Semantik
  • In Go ist manuelle Speicherverwaltung möglich, und sie kann in Zusammenarbeit mit dem GC erfolgen
  • Eine Arena ist eine Datenstruktur, die Speicher mit gleicher Lebensdauer effizient allokiert; erklärt wird, wie sich das in Go umsetzen lässt
  • Es wird erklärt, wie der GC Speicher mit dem Mark-and-Sweep-Algorithmus verwaltet
  • Mit Arenas lässt sich die Speicherallokations-Performance verbessern, was durch verschiedene Optimierungen möglich wird
  • Durch Entfernen der Write Barrier, Wiederverwendung von Speicher, Chunk Pooling usw. wird versucht, die Performance zu steigern und die GC-Last zu minimieren
  • Es werden sichere und schnelle Muster für die Verarbeitung großer Speichermengen in der Praxis vorgestellt, etwa durch Realloc-Implementierung, Arena-Wiederverwendung und Initialisierung (Reset)

Überblick über Arena-basierte manuelle Speicherallokation in Go

  • Go ist dank klarem GC-Verhalten und nahezu fehlendem Undefined Behavior eine sichere Sprache
  • Mit dem Paket unsafe ist eine direkte Kontrolle über Speicher passend zur internen GC-Implementierung möglich
  • Dieser Artikel erklärt, wie man in Go einen Arena-strukturbasierten Speicherallokator baut, der mit dem GC zusammenarbeiten kann

Definition und Zweck von Arenas

  • Eine Arena ist eine Struktur, um Objekte mit derselben Lebensdauer effizient zu allokieren
  • Während ein gewöhnliches append ein Array exponentiell erweitert, fügt eine Arena neue Blöcke hinzu und liefert Pointer zurück
  • Die Standardschnittstelle sieht wie folgt aus:
    • Alloc(size, align uintptr) unsafe.Pointer

Pointer und Funktionsweise des GC

  • Der GC arbeitet, indem er Speicher verfolgt (mark) und einsammelt (sweep)
  • Für einen präzisen GC werden Metadaten namens pointer bits verwendet, die Informationen über Pointer-Positionen liefern
  • Wenn Pointer in einer Arena falsch behandelt werden, kann der GC sie nicht verfolgen, was zu Use-After-Free-Fehlern führen kann

Entwurf einer Arena

  • Die Arena-Struktur hat die folgenden Felder:
    • next, left, cap, chunks
  • Alle Allokationen werden mit 8-Byte-Ausrichtung verarbeitet; reicht der Platz nicht aus, wird mit nextPow2 ein neuer Chunk erzeugt
  • Chunks werden nicht als []uintptr, sondern als struct { A [N]uintptr; P *Arena } allokiert, damit der GC die Arena verfolgen kann

Wie die Pointer-Sicherheit der Arena sichergestellt wird

  • Wenn nur innerhalb der Arena allokierte Pointer verwendet werden, hält der GC die gesamte Arena am Leben
  • Indem Pointer so gesetzt werden, dass sie auf die Arena verweisen, wird das Überleben der gesamten Arena im GC garantiert
  • Die Allokationsmethode der Arena führt Folgendes aus:
    • In allocChunk() wird der Arena-Pointer am Ende des Chunks gespeichert

Ergebnisse der Performance-Benchmarks

  • Gegenüber einfachem new zeigt Arena-Allokation im Schnitt eine 2- bis 4-fache oder höhere Performance-Steigerung
  • Auch in Situationen mit hoher GC-Last zeigt der Arena-Ansatz eine um mehr als das 2-Fache bessere Performance
  • Optimierungen wie das Entfernen der Write Barrier oder die Nutzung von uintptr bringen bei kleinen Allokationen bis zu 20 % Performance-Gewinn

Strategien zur Chunk-Wiederverwendung und Heap-Vermeidung

  • Mit sync.Pool lassen sich Chunks wiederverwenden
  • Über runtime.SetFinalizer() können Chunks in den Pool zurückgegeben werden, wenn die Arena verschwindet
  • Die Performance verbessert sich bei kleinen Allokationen stark, kann bei großen Allokationen aber langsamer als new sein

Initialisierung und Wiederverwendung von Arenas

  • Mit der Methode Reset() lässt sich eine Arena in ihren Anfangszustand zurückversetzen
  • Das ist riskant, ermöglicht aber die Wiederverwendung derselben Struktur ohne erneute Speicherallokation
  • Auch bei der Wiederverwendung werden Chunks wiederverwendet, was die Performance deutlich steigert

Implementierung einer Realloc-Funktion

  • In der Arena wird eine realloc-Funktion implementiert, die die dynamische Erweiterung der zuletzt erfolgten Allokation ermöglicht
  • Ist das nicht möglich, wird neuer Speicher allokiert und anschließend kopiert

Fazit und vollständiger Code

  • Durch tiefes Verständnis des GC-Mechanismus von Go und auf Basis der internen Implementierung entsteht ein Arena-basierter Speicherverwalter
  • Die Struktur vereint Sicherheit und Performance und ist bei angemessenem Einsatz sehr nützlich für die Verarbeitung großer Datenstrukturen
  • Der vollständige Implementierungscode umfasst die Arena-Struktur sowie New, Alloc, Reset, allocChunk, finalize usw.

1 Kommentare

 
GN⁺ 2025-04-24
Hacker-News-Kommentar
  • Dieser Artikel ist eine unterhaltsame Lektüre

    • Wenn dir der Artikel gefallen hat oder du die Speicherallokation in Go besser kontrollieren möchtest, schau dir gern das Paket an, das ich geschrieben habe
    • Ich würde mich über Feedback freuen oder darüber, wenn andere es verwenden
    • Dieses Paket allokiert seinen eigenen Speicher getrennt von der Runtime und umgeht damit den GC vollständig
    • Es erlaubt bei der Allokation keine Zeigertypen, ersetzt sie aber durch einen Reference[T]-Typ mit derselben Funktionalität
    • Die Speicherfreigabe erfolgt manuell, daher kann man sich nicht auf den Garbage Collector verlassen
    • Solche benutzerdefinierten Allokatoren in Go orientieren sich normalerweise an Arenen, die Allokationsgruppen unterstützen, die gemeinsam erzeugt und zerstört werden
    • Das offheap-Paket zielt jedoch darauf ab, große langlebige Datenstrukturen aufzubauen und die Kosten der Garbage Collection auf null zu bringen
    • Zum Beispiel große In-Memory-Caches oder Datenbanken
  • Beim jüngsten Performance-Tuning in Go habe ich ein sehr ähnliches Arena-Design verwendet, um die Leistung zu maximieren

    • Statt unsafe-Zeigern habe ich Byte-Slices als Buffer und Chunks verwendet
    • Ich habe das ausprobiert, aber es war nicht schneller und deutlich komplexer
    • Ich sollte das noch einmal prüfen, bevor ich mir zu 100 % sicher bin
  • Ein paar einfache Verbesserungen

    • Wenn man mit kleinen Slices beginnt und später manche Payloads in großen Mengen hinzukommen, kann man ein eigenes append schreiben, das die cap aggressiver erhöht, bevor das eingebaute append aufgerufen wird
    • unsafe.String ist nützlich, um Strings aus Byte-Slices ohne Allokation weiterzugeben
    • Man sollte die Warnhinweise sorgfältig lesen und verstehen, was man tut
  • Das hat nichts mit dem Thema zu tun, aber mir gefällt die Minimap an der Seite

    • Sie ist nützlich, wenn man lange technische Artikel springend liest oder etwas nachschlagen will, das man zuvor gelesen hat
    • Ich frage mich, wie ich so etwas zu meiner Website hinzufügen könnte
    • Wirklich cool
  • Zusammenfassung für Leute, die lange Artikel nur ungern lesen

    • OP hat in Go mit unsafe einen Arena-Allokator gebaut, um Allokatorarbeit zu beschleunigen
    • Besonders nützlich ist das, wenn viele Dinge allokiert werden, die gemeinsam erzeugt und zerstört werden
    • Das Hauptproblem ist, dass Gos GC das Layout der Daten kennen muss, insbesondere die Positionen von Zeigern, damit er korrekt funktioniert
    • Wenn man rohe Bytes mit unsafe.Pointer allokiert, kann der GC Verweise aus der Arena möglicherweise nicht korrekt sehen und etwas versehentlich freigeben
    • Damit es trotzdem funktioniert, solange Zeiger auf andere Dinge in derselben Arena zeigen, wird die gesamte Arena behalten, wenn noch auf einen Teil von ihr verwiesen wird
    • Dazu wird (1) ein Slice (chunks) beibehalten, das auf alle großen Speicherblöcke zeigt, die die Arena vom System erhalten hat,
    • und (2) reflect.StructOf verwendet, um einen neuen Typ zu erzeugen, der zusätzliche Zeigerfelder auf diese Blöcke enthält
    • Wenn der GC also einen Zeiger auf die Chunks findet, findet er auch die Rückverweise, markiert dadurch die Arena als lebendig und behält das Chunk-Slice bei
    • Danach werden interessante Optimierungstechniken vorgestellt, um verschiedene interne Prüfungen und Write Barriers zu entfernen
  • Verwandt: Diskussion über das Hinzufügen von "Speicherbereichen" zur Standardbibliothek

    • Frühere Arena-Vorschläge
  • Interessanter Stoff

    • Ich frage mich, wie Leute, die in Go Off-Heap- oder Arena-artige Allokatoren bauen, normalerweise Speichersicherheit und GC-Interaktionen testen oder benchmarken
  • Go priorisiert es, das Ökosystem nicht kaputtzumachen

    • Dadurch kann man annehmen, dass Hyrums Gesetz bestimmte beobachtbare Verhaltensweisen der Runtime schützen wird
    • Wenn diese Behauptung stimmt, ist Go als Sprache in einer evolutionären Sackgasse
    • In diesem Fall bin ich nicht sicher, ob Go interessant ist
  • Eine kurze Meta-Anmerkung

    • Dieser Artikel ist wirklich lang, daher hatte ich keine Zeit, die Hintergrunddetails zu lesen
    • Zum Beispiel nimmt der Abschnitt "Mark and Sweep" auf meinem Laptop-Bildschirm mehr als vier Seiten ein
    • Dieser Abschnitt beginnt erst nach mehr als fünf Seiten des Artikels
    • Ich frage mich, ob KI beim Schreiben der Abschnitte geholfen hat und sie dadurch zu umfassend geworden sind
    • Inhalte zu erzeugen ist einfach, aber es werden keine redaktionellen Entscheidungen getroffen, um nur das Wichtige zu behalten
    • Ich möchte nur etwas über Arena-Allokatoren wissen und brauche kein Tutorial über Garbage Collection