Du hast deine Draw Calls profilt, deine Frame Time ist okay, und trotzdem hört das Spiel etwa jede Minute einfach… auf. Kurz. Regelmäßig. Wie ein Uhrwerk.
Das ist der Garbage Collector, und das Intervall ist eine Project Setting namens Time Between Purging Pending Kill Objects — 60 Sekunden per Default. Jetzt weißt du, wonach du suchen musst.
Was tatsächlich passiert
Unreal verwaltet UObject-Speicher mit einem Mark-and-Sweep-Collector. Zwei Phasen:
Reachability Analysis — beim Root Set anfangen und jede Referenz ablaufen. Was erreichbar ist, lebt. Was nicht, ist Müll. Das ist der teure Teil, und historisch läuft er in einem einzigen Frame. Alle UObject-Verarbeitung — Gameplay inklusive — steht still, bis er fertig ist.
Purge — die nicht erreichbaren Objekte tatsächlich freigeben.
Der entscheidende Zusammenhang: die Pause skaliert damit, wie viele Objekte der Collector scannen muss, nicht damit wie viele Müll sind. Eine leere Map mit 500.000 UObjects bezahlt den Lauf trotzdem. Deshalb taucht dein Hitch auf, während dein Projekt wächst, und geht nie mehr weg.
Nur reflektierte Referenzen zählen
Der Collector findet Referenzen über das Reflection-System. Das heißt:
UPROPERTY()-Pointer sind für die GC sichtbar.- Raw Pointer ohne
UPROPERTYsind unsichtbar. Das Objekt dahinter kann unter dir weggesammelt werden — und jetzt hast du einen Dangling Pointer statt eines Hitches.
In Blueprints kriegst du das gratis; jede Variable ist reflektiert. In C++ ist es die Nummer-eins-Quelle für "läuft, bis die GC kommt"-Bugs. Wenn ein UObject-Pointer Member eines UObjects ist, will er UPROPERTY().
Incremental Reachability: der große Hebel
Seit UE 5.4 kannst du die Reachability Analysis über mehrere Frames verteilen, mit einem weichen Zeitlimit pro Frame. Statt eines 40-ms-Spikes bekommst du mehrere kleine Scheiben, die keiner merkt.
CB0
Der letzte Wert ist ein 2-ms-Soft-Budget pro Frame. Profil es in Unreal Insights und du siehst den Spike ersetzt durch breitere, flache Balken.
Zwei ehrliche Einschränkungen, direkt aus Doku und Bug-Tracker:
- Das Referenz-Tracking zwischen Iterationen läuft über
TObjectPtr, nicht über Raw Pointer. Eine Zuweisung an einTObjectPtr-exponiertesUPROPERTYmarkiert das Objekt mitten in der Collection als erreichbar. Raw-UPROPERTY-Pointer machen das nicht auf dieselbe Art mit. - Es ist nicht vollständig thread safe. Ein Objekt, das auf einem Worker Thread bearbeitet wird, wird eventuell nicht als erreichbar markiert, während der Collector scannt — und kann verfrüht gesammelt werden.
Es gibt außerdem gemeldete Probleme zwischen Incremental Reachability und World-Partition-Streaming — UWorld/ULevel-Objekte werden nicht zerstört — und einen Loading-Screen-Deadlock. Anschalten, messen, und speziell Streaming und Ladebildschirme testen. Das ist ein großer Gewinn mit scharfen Kanten, kein Gratis-Schalter.
Die drei Hebel, nach Wirkung sortiert
1. Weniger Objekte haben. Nichts kommt da ran. Jedes UObject ist Scan-Kosten für immer. Hier zahlen sich Soft References und Instancing ein zweites Mal aus: 5000 Instanzen in einer Component sind ein Objekt; 5000 Actors sind 5000.
2. Poolen statt spawnen. Spawn/Destroy-Gewusel erzeugt Müll nach Plan. Projektile, Treffer-Effekte, Schadenszahlen — pool sie. Dieselben Objekte, wiederverwendet, nie gesammelt.
3. Am Intervall drehen. Time Between Purging Pending Kill Objects hochsetzen macht Hitches seltener, aber größer; runtersetzen macht sie häufiger, aber kleiner. Es entfernt keine Arbeit. Nützlich, um Collections in einen Ladebildschirm zu schieben; kein Fix.
GC während des Gameplays abschalten ist eine echte Technik in ausgelieferten Spielen. Es ist auch der Weg, ein Memory Leak zu shippen. Wisse was du tust, bevor du danach greifst.
Messen
stat GC und stat Memory in der Konsole. Unreal Insights, wenn du die Form des Spikes sehen willst. Und obj gc von Hand, um eine Collection zu erzwingen — wenn dein Frame genauso stockt wie alle 60 Sekunden, hast du die Diagnose in zehn Sekunden bestätigt.
Profil den Packaged Build. Editor-GC-Verhalten ist kein Shipping-GC-Verhalten.
Häufige Fehler
- Raw-UObject-Pointer ohne
UPROPERTY. Kein Hitch — ein Crash. Die GC sieht sie nicht und sammelt das Ziel ein. - Deinen Code für ein 60-Sekunden-Stottern verantwortlich machen. Erst
stat GC. Es ist der Collector, bis das Gegenteil bewiesen ist. - Incremental GC anschalten und für erledigt halten. Streaming testen, Ladebildschirme testen, Worker-Thread-Arbeit testen. Es gibt bekannte Probleme.
- Annehmen, Incremental GC funktioniert mit Raw Pointern. Es trackt über
TObjectPtr. - Das GC-Intervall hochsetzen um das Problem zu verstecken. Du hast den Hitch seltener und schlimmer gemacht.
- Per-Frame-Objekte spawnen und zerstören. Das ist ein GC-Zeitplan, den du selbst gebaut hast.
30-Sekunden-Recap
- Der 60-Sekunden-Hitch ist die Reachability Analysis, und das Intervall ist eine Project Setting.
- Die Pause skaliert mit der Gesamt-Objektzahl, nicht mit der Müllmenge.
- Kein
UPROPERTYheißt unsichtbar für die GC — Dangling Pointer, kein Stottern. - UE 5.4+:
gc.AllowIncrementalReachability=1verteilt die Pause über Frames. BrauchtTObjectPtr, nicht voll thread safe, bekannte Streaming-Probleme. - Weniger Objekte > Pooling > Intervall-Tuning. In der Reihenfolge.
stat GC,obj gc, Unreal Insights. Packaged Build.
Nächsten Sonntag: Lumen für Anfänger — Software vs Hardware Ray Tracing, was wirklich Frames kostet, und warum die meisten Tuning-Tipps online aus 2023 sind.
— Marco
Comments
Leave a comment