
Leistungsprofilierungstipps für Spieleentwickler
Eine reibungslose Leistung ist entscheidend für die Schaffung immersiver Spielerlebnisse. Um sicherzustellen, dass Ihr Spiel optimiert ist, ist ein konsistenter, durchgängiger Profilierungsworkflow ein "Muss" für eine effiziente Spieleentwicklung, und er beginnt mit einem einfachen dreipunktigen Verfahren:
- Profilieren Sie, bevor Sie größere Änderungen vornehmen: Legen Sie eine Basislinie fest.
- Profilierung während der Entwicklung: Verfolgen Sie und stellen Sie sicher, dass Änderungen die Leistung oder Budgets nicht beeinträchtigen.
- Profilieren Sie danach: Beweisen Sie, dass die Änderungen die gewünschte Wirkung hatten.
Diese Seite skizziert einen allgemeinen Profilierungsworkflow für Spieleentwickler. Es ist aus dem E-Book, Ultimative Anleitung zur Profilierung von Unity-Spielen, das kostenlos heruntergeladen werden kann (die Unity 6-Version des Leitfadens wird bald verfügbar sein). Das E-Book wurde sowohl von externen als auch von internen Unity-Experten in den Bereichen Spieleentwicklung, Profilierung und Optimierung erstellt.
In diesem Artikel erfahren Sie, welche nützlichen Ziele Sie mit der Profilierung setzen können, häufige Leistungsengpässe, wie z.B. CPU-gebunden oder GPU-gebunden, und wie Sie diese Situationen genauer identifizieren und untersuchen können.

Setzen Sie ein Frame-Budget
Spieler messen die Leistung oft anhand der Bildrate oder Bilder pro Sekunde (fps), aber als Entwickler wird allgemein empfohlen, stattdessen Bildzeit in Millisekunden zu verwenden. Betrachten Sie das folgende vereinfachte Szenario:
Während der Laufzeit rendert Ihr Spiel 59 Bilder in 0,75 Sekunden. Das nächste Bild benötigt jedoch 0,25 Sekunden zum Rendern. Die durchschnittliche ausgelieferte Bildrate von 60 fps klingt gut, aber in Wirklichkeit werden die Spieler einen Ruckeleffekt bemerken, da das letzte Bild ein Viertel einer Sekunde zum Rendern benötigt.
Das ist einer der Gründe, warum es wichtig ist, ein bestimmtes Zeitbudget pro Bild anzustreben. Dies gibt Ihnen ein solides Ziel, auf das Sie hinarbeiten können, wenn Sie Ihr Spiel profilieren und optimieren, und letztendlich schafft es ein reibungsloseres und konsistenteres Erlebnis für Ihre Spieler.
Jedes Bild hat ein Zeitbudget basierend auf Ihrer Ziel-fps. Eine Anwendung, die auf 30 fps abzielt, sollte immer weniger als 33,33 ms pro Bild benötigen (1000 ms / 30 fps). Ebenso lässt ein Ziel von 60 fps 16,66 ms pro Bild (1000 ms / 60 fps).
Sie können dieses Budget während nicht-interaktiver Sequenzen überschreiten, zum Beispiel beim Anzeigen von UI-Menüs oder beim Laden von Szenen, aber nicht während des Spiels. Selbst ein einzelnes Bild, das das Ziel-Bildbudget überschreitet, wird Ruckler verursachen.
Hinweis: Eine konstant hohe Bildrate in VR-Spielen ist entscheidend, um Übelkeit oder Unbehagen bei den Spielern zu vermeiden, und ist oft notwendig, damit Ihr Spiel die Zertifizierung des Plattforminhabers erhält.
Bilder pro Sekunde: Eine irreführende Kennzahl
Eine gängige Methode, wie Spieler die Leistung messen, ist die Bildrate oder Bilder pro Sekunde. Es wird jedoch empfohlen, stattdessen die Bildzeit in Millisekunden zu verwenden. Um zu verstehen, warum, schauen Sie sich das obige Diagramm von fps im Vergleich zur Bildzeit an.
Berücksichtigen Sie diese Zahlen:
1000 ms/sec / 900 fps = 1.111 ms pro Frame
1000 ms/sec / 450 fps = 2.222 ms pro Frame
1000 ms/sec / 60 fps = 16.666 ms pro Frame
1000 ms/sec / 56.25 fps = 17.777 ms pro Frame
Wenn Ihre Anwendung mit 900 fps läuft, entspricht das einer Frame-Zeit von 1.111 Millisekunden pro Frame. Bei 450 fps sind das 2.222 Millisekunden pro Frame. Das stellt einen Unterschied von nur 1.111 Millisekunden pro Frame dar, obwohl die Bildrate scheinbar um die Hälfte sinkt.
Wenn Sie die Unterschiede zwischen 60 fps und 56.25 fps betrachten, entspricht das 16.666 Millisekunden pro Frame und 17.777 Millisekunden pro Frame, jeweils. Das stellt ebenfalls 1.111 Millisekunden zusätzlich pro Frame dar, aber hier fühlt sich der Rückgang der Bildrate prozentual viel weniger dramatisch an.
Deshalb verwenden Entwickler die durchschnittliche Frame-Zeit, um die Spielgeschwindigkeit zu benchmarken, anstatt fps.
Machen Sie sich keine Sorgen um fps, es sei denn, Sie fallen unter Ihre Ziel-Bildrate. Konzentrieren Sie sich auf die Frame-Zeit, um zu messen, wie schnell Ihr Spiel läuft, und bleiben Sie dann innerhalb Ihres Frame-Budgets.
Lesen Sie den Originalartikel, „Robert Dunlops fps versus Frame-Zeit“, für weitere Informationen.

Herausforderungen für mobile Geräte
Thermische Kontrolle ist eines der wichtigsten Bereiche, die bei der Entwicklung von Anwendungen für mobile Geräte optimiert werden müssen. Wenn die CPU oder GPU zu lange mit voller Leistung arbeiten, aufgrund ineffizienter Codes, werden diese Chips heiß. Um Überhitzung und potenzielle Schäden an den Chips zu vermeiden, wird das Betriebssystem die Taktrate des Geräts reduzieren, um es abkühlen zu lassen, was zu Frame-Ruckeln und einer schlechten Benutzererfahrung führt. Diese Leistungsreduzierung ist als thermisches Drosseln bekannt.
Höhere Bildraten und erhöhte Codeausführung (oder DRAM-Zugriffsoperationen) führen zu einem erhöhten Batterieverbrauch und Wärmeentwicklung. Schlechte Leistung kann Ihr Spiel auch für ganze Segmente von Geräten mit niedrigerer Leistung unspielbar machen, was zu verpassten Marktchancen führen kann.
Wenn Sie sich mit dem Problem der Wärmeentwicklung befassen, sollten Sie das Budget berücksichtigen, mit dem Sie als systemweites Budget arbeiten können.
Bekämpfen Sie thermisches Drosseln und Batterieverbrauch, indem Sie frühzeitig profilieren, um Ihr Spiel von Anfang an zu optimieren. Stellen Sie Ihre Projekteinstellungen für die Hardware der Zielplattform ein, um thermischen Problemen und Batterieverbrauch entgegenzuwirken.
Passen Sie die Frame-Budgets auf Mobilgeräten an
Ein allgemeiner Tipp zur Bekämpfung von thermischen Problemen bei Geräten über längere Spielzeiten ist, eine Frame-Leerlaufzeit von etwa 35 % zu lassen. Dies gibt mobilen Chips Zeit, sich abzukühlen, und hilft, übermäßigen Batterieverbrauch zu verhindern. Bei einer Ziel-Frame-Zeit von 33,33 ms pro Frame (für 30 fps) beträgt das Frame-Budget für mobile Geräte ungefähr 22 ms pro Frame.
Die Berechnung sieht folgendermaßen aus: (1000 ms / 30) * 0.65 = 21.66 ms
Um 60 fps auf Mobilgeräten zu erreichen, würde die gleiche Berechnung eine Ziel-Frame-Zeit von (1000 ms / 60) * 0.65 = 10.83 ms erfordern. Dies ist auf vielen mobilen Geräten schwierig zu erreichen und würde die Batterie doppelt so schnell entladen wie bei 30 fps. Aus diesen Gründen zielen viele mobile Spiele auf 30 fps statt auf 60 ab. Verwenden Sie Application.targetFrameRate, um diese Einstellung zu steuern, und beziehen Sie sich auf den Abschnitt "Setzen Sie ein Frame-Budget" im Profiling-E-Book für weitere Details zur Frame-Zeit.
Die Frequenzskalierung auf mobilen Chips kann es schwierig machen, Ihre Zuweisungen für die Frame-Leerlaufzeit beim Profilieren zu identifizieren. Ihre Verbesserungen und Optimierungen können einen netto positiven Effekt haben, aber das mobile Gerät könnte die Frequenz herunterskalieren und dadurch kühler laufen. Verwenden Sie benutzerdefinierte Tools wie FTrace oder Perfetto, um die Frequenzen mobiler Chips, Leerlaufzeiten und Skalierungen vor und nach Optimierungen zu überwachen.
Solange Sie innerhalb Ihres gesamten Frame-Zeitbudgets für Ihre Ziel-fps (sagen wir 33,33 ms für 30 fps) bleiben und sehen, dass Ihr Gerät weniger arbeitet oder niedrigere Temperaturen aufzeichnet, um diese Bildrate aufrechtzuerhalten, sind Sie auf dem richtigen Weg.
Ein weiterer Grund, um Spielraum im Frame-Budget auf mobilen Geräten hinzuzufügen, ist, um reale Temperaturschwankungen zu berücksichtigen. An einem heißen Tag wird ein mobiles Gerät heiß und hat Schwierigkeiten, Wärme abzuführen, was zu thermischem Drosseln und schlechter Spielleistung führen kann. Setzen Sie einen Prozentsatz des Frame-Budgets beiseite, um dieses Szenario zu vermeiden.

Reduzieren Sie Speicherzugriffsoperationen
DRAM-Zugriff ist typischerweise ein energiehungriger Vorgang auf mobilen Geräten. Die Optimierungsanleitung von Arm für Grafikinhalt auf mobilen Geräten besagt, dass der Zugriff auf LPDDR4-Speicher ungefähr 100 Pikojoule pro Byte kostet.
Reduzieren Sie die Anzahl der Speicherzugriffsoperationen pro Frame durch:
- Reduzierung der Bildrate
- Reduzierung der Bildschirmauflösung, wo möglich
- Verwendung einfacherer Netze mit reduzierter Anzahl von Scheitelpunkten und Attributgenauigkeit
- Verwendung von Texturkompression und Mipmapping
Wenn Sie sich auf Geräte konzentrieren müssen, die Arm-CPU- oder GPU-Hardware nutzen, enthält das Arm Performance Studio-Tool (insbesondere der Streamline Performance Analyzer) einige großartige Leistungszähler zur Identifizierung von Speicherbandbreitenproblemen. Die verfügbaren Zähler sind in einem entsprechenden Benutzerhandbuch für jede Arm-GPU-Generation aufgeführt und erklärt, zum Beispiel im Mali-G710 Performance Counter Reference Guide. Beachten Sie, dass das GPU-Profiling von Arm Performance Studio eine Arm Immortalis oder Mali GPU erfordert.
Stellen Sie Hardware-Tiers für Benchmarking auf
Zusätzlich zur Verwendung plattformspezifischer Profiling-Tools sollten Sie Tiers oder ein Gerät mit den niedrigsten Spezifikationen für jede Plattform und Qualitätsstufe, die Sie unterstützen möchten, festlegen und dann die Leistung für jede dieser Spezifikationen profilieren und optimieren.
Als Beispiel, wenn Sie mobile Plattformen anvisieren, könnten Sie entscheiden, drei Tiers mit Qualitätskontrollen zu unterstützen, die Funktionen basierend auf der Zielhardware ein- oder ausschalten. Sie optimieren dann für die niedrigste Gerätespezifikation in jedem Tier. Als weiteres Beispiel, wenn Sie ein Spiel für Konsolen entwickeln, stellen Sie sicher, dass Sie sowohl auf älteren als auch auf neueren Versionen profilieren.
Unser neuester Leitfaden zur mobilen Optimierung enthält viele Tipps und Tricks, die Ihnen helfen werden, die thermische Drosselung zu reduzieren und die Akkulaufzeit für mobile Geräte, die Ihre Spiele ausführen, zu erhöhen.
Von der Hoch- zur Niedrigprofilierung
Beim Profiling möchten Sie sicherstellen, dass Sie Ihre Zeit und Mühe auf Bereiche konzentrieren, in denen Sie den größten Einfluss erzielen können. Daher wird empfohlen, mit einem Top-Down-Ansatz beim Profiling zu beginnen, was bedeutet, dass Sie mit einem Überblick über Kategorien wie Rendering, Skripte, Physik und Garbage Collection (GC)-Zuweisungen beginnen. Sobald Sie problematische Bereiche identifiziert haben, können Sie in die tiefergehenden Details eintauchen. Verwenden Sie diesen Überblick, um Daten zu sammeln und Notizen zu den kritischsten Leistungsproblemen zu machen, einschließlich Szenarien, die unerwünschte verwaltete Zuweisungen oder übermäßige CPU-Nutzung in Ihrer Hauptspielschleife verursachen.
Zuerst müssen Sie die Aufrufstapel für GC.Alloc-Markierungen sammeln. Wenn Sie mit diesem Prozess nicht vertraut sind, finden Sie einige Tipps und Tricks im Abschnitt mit dem Titel "Wiederkehrende Speicherzuweisungen über die Lebensdauer der Anwendung lokalisieren" im E-Book.
Wenn die gemeldeten Aufrufstapel nicht detailliert genug sind, um die Quelle der Zuweisungen oder anderer Verlangsamungen zu verfolgen, können Sie eine zweite Profilsitzung mit aktiviertem Deep Profiling durchführen, um die Quelle der Zuweisungen zu finden. Wir behandeln Deep Profiling im E-Book ausführlicher, aber zusammenfassend ist es ein Modus im Profiler, der detaillierte Leistungsdaten für jeden Funktionsaufruf erfasst und granulare Einblicke in Ausführungszeiten und -verhalten bietet, jedoch mit erheblich höherem Overhead im Vergleich zum Standardprofiling.
Wenn Sie Notizen zu den "Übeltätern" der Frame-Zeit sammeln, achten Sie darauf, wie sie im Vergleich zum Rest des Frames abschneiden. Dieser relative Einfluss kann verzerrt werden, wenn Deep Profiling aktiviert ist, da Deep Profiling erheblichen Overhead hinzufügt, indem jeder Methodenaufruf instrumentiert wird.
Profilieren Sie früh
Obwohl Sie während des gesamten Entwicklungszyklus Ihres Projekts immer profilieren sollten, werden die bedeutendsten Gewinne aus dem Profiling erzielt, wenn Sie in den frühen Phasen beginnen.
Profilieren Sie früh und oft, damit Sie und Ihr Team eine "Leistungsunterschrift" für das Projekt verstehen und sich merken können, die Sie als Benchmark verwenden können. Wenn die Leistung stark abfällt, können Sie leicht erkennen, wann etwas schiefgeht, und das Problem beheben.
Während das Profilieren im Editor Ihnen eine einfache Möglichkeit bietet, die Hauptprobleme zu identifizieren, kommen die genauesten Profilierungsergebnisse immer von der Ausführung und dem Profilieren von Builds auf Zielgeräten, zusammen mit der Nutzung plattformspezifischer Werkzeuge, um die Hardwaremerkmale jeder Plattform zu untersuchen. Diese Kombination bietet Ihnen einen ganzheitlichen Überblick über die Anwendungsleistung auf all Ihren Zielgeräten. Zum Beispiel könnten Sie auf einigen mobilen Geräten GPU-gebunden sein, auf anderen jedoch CPU-gebunden, und das können Sie nur lernen, indem Sie auf diesen Geräten messen.

Identifizieren von Performance-Problemen
Laden Sie die druckbare PDF-Version dieses Diagramms hier herunter.
Der Zweck des Profilierens besteht darin, Engpässe als Ziele für die Optimierung zu identifizieren. Wenn Sie auf Vermutungen angewiesen sind, können Sie Teile des Spiels optimieren, die keine Engpässe sind, was zu wenig oder keiner Verbesserung der Gesamtleistung führt. Einige "Optimierungen" könnten sogar die Gesamtleistung Ihres Spiels verschlechtern, während andere arbeitsintensiv sein können, aber unbedeutende Ergebnisse liefern. Der Schlüssel besteht darin, die Auswirkungen Ihrer fokussierten Zeitinvestition zu optimieren.
Das Flussdiagramm oben veranschaulicht den anfänglichen Profilierungsprozess, wobei die folgenden Abschnitte detaillierte Informationen zu jedem Schritt bieten. Sie präsentieren auch Profiler-Aufnahmen aus echten Unity-Projekten, um die Arten von Dingen zu veranschaulichen, nach denen man suchen sollte.
Um ein ganzheitliches Bild aller CPU-Aktivitäten zu erhalten, einschließlich der Zeiten, in denen sie auf die GPU wartet, verwenden Sie die Timeline-Ansicht im CPU-Modul des Profiler. Machen Sie sich mit den üblichen Profiler-Markern vertraut, um Aufnahmen korrekt zu interpretieren. Einige der Profiler-Marker können je nach Zielplattform unterschiedlich erscheinen, daher sollten Sie sich Zeit nehmen, um Aufnahmen Ihres Spiels auf jeder Ihrer Zielplattformen zu erkunden, um ein Gefühl dafür zu bekommen, wie eine "normale" Aufnahme für Ihr Projekt aussieht.
Die Leistung eines Projekts wird durch den Chip und/oder den Thread begrenzt, der am längsten dauert. Das ist der Bereich, auf den sich die Optimierungsbemühungen konzentrieren sollten. Stellen Sie sich beispielsweise die folgenden Szenarien für ein Spiel mit einem Ziel-Frame-Zeitbudget von 33,33 ms und aktiviertem VSync vor:
- Wenn die CPU-Frame-Zeit (ohne VSync) 25 ms beträgt und die GPU-Zeit 20 ms beträgt, kein Problem! Sie sind CPU-gebunden, aber alles liegt im Budget, und Optimierungen werden die Bildrate nicht verbessern (es sei denn, Sie bringen sowohl CPU als auch GPU unter 16,66 ms und springen auf 60 fps).
- Wenn die CPU-Frame-Zeit 40 ms beträgt und die GPU 20 ms beträgt, sind Sie CPU-gebunden und müssen die CPU-Leistung optimieren. Die Optimierung der GPU-Leistung wird nicht helfen; tatsächlich möchten Sie möglicherweise einige der CPU-Arbeiten auf die GPU verlagern, indem Sie beispielsweise Compute-Shader anstelle von C#-Code verwenden, wo dies anwendbar ist, um die Dinge auszugleichen.
- Wenn die CPU-Frame-Zeit 20 ms beträgt und die GPU 40 ms beträgt, sind Sie GPU-gebunden und müssen die GPU-Arbeiten optimieren.
- Wenn CPU und GPU beide bei 40 ms liegen, sind Sie durch beide begrenzt und müssen beide unter 33,33 ms optimieren, um 30 fps zu erreichen.
Sehen Sie sich diese Ressourcen an, die weiter erkunden, ob Sie CPU- oder GPU-gebunden sind:

Sind Sie innerhalb der Frame-Planung?
Das Profilieren und Optimieren Ihres Projekts frühzeitig und häufig während der Entwicklung wird Ihnen helfen, sicherzustellen, dass alle CPU-Threads Ihrer Anwendung und die gesamte GPU-Frame-Zeit innerhalb des Frame-Budgets liegen. Die Frage, die diesen Prozess leiten wird, ist, ob Sie innerhalb des Frame-Budgets sind oder nicht?
Oben ist ein Bild einer Profilaufnahme aus einem Unity-Mobilspiel, das von einem Team entwickelt wurde, das kontinuierliches Profiling und Optimierung durchgeführt hat. Das Spiel zielt auf 60 fps auf Hochleistungsmobiltelefonen und 30 fps auf mittel- bis niedrigleistungsfähigen Telefonen, wie dem in dieser Aufnahme.
Beachten Sie, dass fast die Hälfte der Zeit im ausgewählten Frame von dem gelben WaitForTargetFPS Profiler-Marker eingenommen wird. Die Anwendung hat Application.targetFrameRate auf 30 fps gesetzt, und VSync ist aktiviert. Die tatsächliche Verarbeitungsarbeit im Hauptthread endet etwa bei der 19 ms-Marke, und die restliche Zeit wird damit verbracht, auf das Verstreichen der verbleibenden 33,33 ms zu warten, bevor der nächste Frame beginnt. Obwohl diese Zeit mit einem Profiler-Marker dargestellt wird, ist der Haupt-CPU-Thread während dieser Zeit im Wesentlichen inaktiv, was es der CPU ermöglicht, sich abzukühlen und eine minimale Batterieleistung zu verwenden.
Der Marker, auf den man achten sollte, könnte auf anderen Plattformen oder wenn VSync deaktiviert ist, unterschiedlich sein. Das Wichtige ist zu überprüfen, ob der Hauptthread innerhalb Ihres Frame-Budgets läuft oder genau auf Ihrem Frame-Budget mit einer Art Marker, der anzeigt, dass die Anwendung auf VSync wartet und ob die anderen Threads eine Leerlaufzeit haben.
Leerlaufzeit wird durch graue oder gelbe Profiler-Marker dargestellt. Der Screenshot oben zeigt, dass der Render-Thread in Gfx.WaitForGfxCommandsFromMainThread im Leerlauf ist, was Zeiten anzeigt, in denen er das Senden von Draw-Calls an die GPU in einem Frame abgeschlossen hat und auf weitere Draw-Call-Anfragen von der CPU im nächsten wartet. Ähnlich verbringt der Job Worker 0 Thread zwar etwas Zeit in Canvas.GeometryJob, aber die meiste Zeit ist er im Leerlauf. Dies sind alles Anzeichen für eine Anwendung, die bequem innerhalb des Frame-Budgets liegt.
Wenn Ihr Spiel im Frame-Budget ist
Wenn Sie innerhalb des Frame-Budgets sind, einschließlich aller Anpassungen, die am Budget vorgenommen wurden, um den Batterieverbrauch und die thermische Drosselung zu berücksichtigen, sind Sie mit den wichtigsten Profilierungsaufgaben fertig. Sie können abschließen, indem Sie den Memory Profiler ausführen, um sicherzustellen, dass die Anwendung auch innerhalb ihres Speicherbudgets liegt.
Das Bild oben zeigt ein Spiel, das bequem innerhalb des ~22 ms Frame-Budgets läuft, das für 30 fps erforderlich ist. Beachten Sie das WaitForTargetfps, das die Hauptthread-Zeit bis VSync und die grauen Leerlaufzeiten im Render-Thread und im Worker-Thread polstert. Beachten Sie auch, dass das VBlank-Intervall beobachtet werden kann, indem man sich die Endzeiten von Gfx.Present Frame über Frame ansieht, und dass Sie eine Zeitskala im Timeline-Bereich oder auf dem Zeitmaßstab oben zeichnen können, um von einem dieser zum nächsten zu messen.

CPU-gebunden
Wenn Ihr Spiel nicht innerhalb des CPU-Frame-Budgets ist, besteht der nächste Schritt darin, zu untersuchen, welcher Teil der CPU der Engpass ist – mit anderen Worten, welcher Thread am meisten beschäftigt ist.
Es ist selten, dass die gesamte CPU-Arbeitslast der Engpass ist. Moderne CPUs haben eine Reihe von verschiedenen Kernen, die in der Lage sind, unabhängig und gleichzeitig zu arbeiten. Verschiedene Threads können auf jedem CPU-Kern ausgeführt werden. Eine vollständige Unity-Anwendung verwendet eine Reihe von Threads für verschiedene Zwecke, aber die, die am häufigsten zur Auffindung von Leistungsproblemen verwendet werden, sind:
- Der Hauptthread: Hier führt die Mehrheit der Spiel-Logik/Skripte standardmäßig ihre Arbeit aus. Die meisten Unity-Systeme, wie Physik, Animation, UI und die ersten Phasen des Renderns, führen hier aus.
- Der Render-Thread: Dieser kümmert sich um die Vorbereitungsarbeiten (z. B. welche Objekte in der Szene für die Kamera sichtbar sind und welche ausgeschlossen/unsichtbar sind, weil sie außerhalb des Sichtfrustums liegen, verdeckt sind oder aus anderen Kriterien herausgefiltert werden), die geschehen müssen, bevor Renderanweisungen an die GPU gesendet werden.
- Während des Renderprozesses untersucht der Hauptthread die Szene und führt Kamera-Culling, Tiefensortierung und Draw-Call-Batching durch, was zu einer Liste von Dingen führt, die gerendert werden sollen. Diese Liste wird an den Render-Thread übergeben, der sie von Unitys interner plattformunabhängiger Darstellung in die spezifischen Grafik-API-Aufrufe übersetzt, die erforderlich sind, um die GPU auf einer bestimmten Plattform zu instruieren.
- Die Job-Arbeiter-Threads: Entwickler können das Job-System nutzen, um bestimmte Arten von Arbeiten auf Arbeiter-Threads zu planen, was die Arbeitslast des Hauptthreads reduziert. Einige von Unitys Systemen und Funktionen nutzen ebenfalls das Job-System, wie Physik, Animation und Rendering.
Ein reales Beispiel für die Optimierung des Hauptthreads
Das Bild unten zeigt, wie die Dinge in einem Projekt aussehen könnten, das durch den Hauptthread gebunden ist. Dieses Projekt läuft auf einem Meta Quest 2, das normalerweise Frame-Budgets von 13,88 ms (72 fps) oder sogar 8,33 ms (120 fps) anstrebt, da hohe Bildraten wichtig sind, um Motion Sickness bei VR-Geräten zu vermeiden. Es ist jedoch klar, dass dieses Projekt in Schwierigkeiten ist, selbst wenn dieses Spiel 30 fps anstrebt.
Obwohl der Render-Thread und die Arbeiter-Threads dem Beispiel ähneln, das innerhalb des Frame-Budgets liegt, ist der Hauptthread während des gesamten Frames eindeutig mit Arbeit beschäftigt. Selbst unter Berücksichtigung des geringen Profiler-Overheads am Ende des Frames ist der Hauptthread über 45 ms beschäftigt, was bedeutet, dass dieses Projekt Bildraten von weniger als 22 fps erreicht. Es gibt keinen Marker, der zeigt, dass der Hauptthread untätig auf VSync wartet; er ist während des gesamten Frames beschäftigt.
Die nächste Phase der Untersuchung besteht darin, die Teile des Frames zu identifizieren, die am längsten dauern, und zu verstehen, warum das so ist. In diesem Frame benötigt PostLateUpdate.FinishFrameRendering 16,23 ms, mehr als das gesamte Frame-Budget. Eine genauere Untersuchung zeigt, dass es fünf Instanzen eines Markers namens Inl_RenderCameraStack gibt, was darauf hinweist, dass fünf Kameras aktiv sind und die Szene rendern. Da jede Kamera in Unity die gesamte Render-Pipeline aufruft, einschließlich Culling, Sortierung und Batching, besteht die höchste Priorität für dieses Projekt darin, die Anzahl aktiver Kameras idealerweise auf nur eine zu reduzieren.
BehaviourUpdate, der Profiler-Marker, der alle Ausführungen der Methode MonoBehaviour.Update() umfasst, benötigt in diesem Frame 7,27 Millisekunden.
Im Timeline-Ansicht zeigen magentafarbene Abschnitte Punkte an, an denen Skripte verwalteten Heap-Speicher zuweisen. Der Wechsel zur Hierarchy-Ansicht und das Filtern durch Eingabe von GC.Alloc in die Suchleiste zeigt, dass die Zuweisung dieses Speichers in diesem Frame etwa 0,33 ms dauert. Das ist jedoch eine ungenaue Messung der Auswirkungen, die die Speicherzuweisungen auf die CPU-Leistung haben.
GC.Alloc-Marker werden nicht zeitlich erfasst, indem ein Begin- und Endpunkt wie bei typischen Profiler-Proben aufgezeichnet wird. Um ihren Overhead zu minimieren, zeichnet Unity nur den Zeitstempel der Zuweisung und die zugewiesene Größe auf.
Der Profiler weist GC.Alloc-Markern eine kleine, künstliche Proben-Dauer zu, um sicherzustellen, dass sie in den Profiler-Ansichten sichtbar sind. Die tatsächliche Zuweisung kann länger dauern, insbesondere wenn ein neuer Speicherbereich vom System angefordert werden muss. Um die Auswirkungen klarer zu sehen, platzieren Sie Profiler-Marker um den Code, der die Zuweisung vornimmt; im tiefen Profiling geben die Lücken zwischen den magentafarbene GC.Alloc-Proben in der Timeline-Ansicht einen Hinweis darauf, wie lange sie möglicherweise gedauert haben.
Darüber hinaus kann die Zuweisung neuer Speicher negative Auswirkungen auf die Leistung haben, die schwerer zu messen und direkt zuzuordnen sind:
- Die Anforderung neuer Speicher vom System kann das Leistungsbudget auf einem mobilen Gerät beeinflussen, was dazu führen kann, dass das System die CPU oder GPU verlangsamt.
- Der neue Speicher muss wahrscheinlich in den L1-Cache der CPU geladen werden und verdrängt damit vorhandene Cache-Zeilen.
- Inkrementelle oder synchrone Garbage Collection kann direkt oder mit Verzögerung ausgelöst werden, wenn der vorhandene freie Speicherplatz im verwalteten Speicher schließlich überschritten wird.
Zu Beginn des Frames summieren sich vier Instanzen von Physics.FixedUpdate auf 4,57 ms. Später benötigen LateBehaviourUpdate (Aufrufe von MonoBehaviour.LateUpdate()) 4 ms, und Animators machen etwa 1 ms aus. Um sicherzustellen, dass dieses Projekt sein gewünschtes Frame-Budget und seine Rate erreicht, müssen alle diese Probleme des Hauptthreads untersucht werden, um geeignete Optimierungen zu finden.
Häufige Fallstricke für Engpässe im Hauptthread
Die größten Leistungsgewinne werden durch die Optimierung der Dinge erzielt, die am längsten dauern. Die folgenden Bereiche sind oft fruchtbare Stellen, um in Projekten, die an den Hauptthread gebunden sind, nach Optimierungen zu suchen:
- Physikberechnungen
- MonoBehaviour-Skriptaktualisierungen
- Speicherzuweisung und/oder -sammlung
- Kameraculling und Rendering im Hauptthread
- Ineffizientes Batchen von Draw Calls
- UI-Aktualisierungen, Layouts und Neuaufbauten
- Animation
Lesen Sie unsere Optimierungsleitfäden, die eine lange Liste umsetzbarer Tipps zur Optimierung einiger der häufigsten Fallstricke bieten:
Je nach dem Problem, das Sie untersuchen möchten, können auch andere Tools hilfreich sein:
- Für MonoBehaviour-Skripte, die lange dauern, aber nicht genau zeigen, warum das der Fall ist, fügen Sie Profiler-Markierungen zum Code hinzu oder versuchen Sie Deep Profiling, um den vollständigen Aufrufstapel zu sehen.
- Für Skripte, die verwalteten Speicher zuweisen, aktivieren Sie Zuweisungsaufrufstapel, um genau zu sehen, woher die Zuweisungen kommen. Alternativ aktivieren Sie Deep Profiling oder verwenden Sie Project Auditor, der Codeprobleme nach Speicher gefiltert anzeigt, sodass Sie alle Codezeilen identifizieren können, die zu verwalteten Zuweisungen führen.
- Verwenden Sie den Frame Debugger, um die Ursachen für schlechtes Batchen von Draw Calls zu untersuchen.
Für umfassende Tipps zur Optimierung Ihres Spiels laden Sie diese Unity-Expertenleitfäden kostenlos herunter:

CPU-gebunden: Render-Thread
Hier ist ein tatsächliches Projekt, das durch seinen Render-Thread gebunden ist. Dies ist ein Konsolenspiel mit isometrischer Ansicht und einem Ziel-Frame-Budget von 33,33 ms.
Die Profileraufnahme zeigt, dass der Hauptthread wartet, bis der Render-Thread bereit ist, bevor das Rendering für den aktuellen Frame beginnen kann, wie durch den Gfx.WaitForPresentOnGfxThread Marker angezeigt. Der Render-Thread reicht immer noch Draw Call-Befehle vom vorherigen Frame ein und ist nicht bereit, neue Draw Calls vom Hauptthread zu akzeptieren; er verbringt auch Zeit in Camera.Render.
Sie können den Unterschied zwischen Markierungen, die sich auf den aktuellen Frame beziehen, und Markierungen von anderen Frames erkennen, da letztere dunkler erscheinen. Sie können auch sehen, dass der Render-Thread über 100 ms benötigt, um den aktuellen Frame zu verarbeiten, sobald der Hauptthread fortfahren kann und beginnt, Draw Calls für den Render-Thread auszugeben, was auch während des nächsten Frames einen Engpass verursacht.
Weitere Untersuchungen zeigten, dass dieses Spiel eine komplexe Rendering-Konfiguration hatte, die neun verschiedene Kameras und viele zusätzliche Durchläufe aufgrund von Ersatz-Shadern umfasste. Das Spiel renderte auch über 130 Punktlichter mit einem Forward-Rendering-Pfad, was für jedes Licht mehrere zusätzliche transparente Draw-Calls hinzufügen kann. Insgesamt führten diese Probleme zu über 3000 Draw-Calls pro Frame.
Häufige Fallstricke für Engpässe im Render-Thread
Die folgenden sind häufige Ursachen, die für Projekte, die an den Render-Thread gebunden sind, untersucht werden sollten:
- Schlechtes Draw-Call-Batching: Dies gilt insbesondere für ältere Grafik-APIs wie OpenGL oder DirectX 11.
- Zu viele Kameras: Es sei denn, Sie erstellen ein Split-Screen-Multiplayer-Spiel, sollten Sie in der Regel nur eine aktive Kamera haben.
- Schlechtes Culling: Dies führt dazu, dass zu viele Dinge gezeichnet werden. Untersuchen Sie die Frustum-Dimensionen Ihrer Kamera und die Culling-Layer-Masken.
Das Rendering Profiler-Modul zeigt eine Übersicht über die Anzahl der Draw-Call-Batches und SetPass-Aufrufe pro Frame. Das beste Werkzeug zur Untersuchung, welche Draw-Call-Batches Ihr Render-Thread an die GPU sendet, ist der Frame-Debugger.
Werkzeuge zur Lösung der identifizierten Engpässe
Während der Fokus dieses E-Books auf der Identifizierung von Leistungsproblemen liegt, bieten die beiden ergänzenden Leistungsoptimierungsleitfäden, die wir zuvor hervorgehoben haben, Vorschläge zur Lösung der Engpässe, abhängig davon, ob Ihre Zielplattform PC oder Konsole oder mobil ist. Im Kontext von Engpässen im Render-Thread ist es erwähnenswert, dass Unity verschiedene Batching-Systeme und Optionen anbietet, abhängig von den Problemen, die Sie identifiziert haben. Hier ist eine schnelle Übersicht über einige der Optionen, die wir im Detail in den E-Books erklären:
- SRP-Batching reduziert die CPU-Überlastung, indem Materialdaten dauerhaft im GPU-Speicher gespeichert werden. Obwohl es die tatsächliche Anzahl der Draw-Calls nicht reduziert, macht es jeden Draw-Call günstiger.
- GPU-Instanzierung kombiniert mehrere Instanzen des gleichen Meshs, die dasselbe Material verwenden, in einem einzigen Draw-Call.
- Statische Batching kombiniert statische (nicht bewegliche) Meshes, die dasselbe Material teilen, und kann somit Vorteile bieten, wenn man mit einem Level-Design mit vielen statischen Elementen arbeitet.
- GPU-residenter Drawer verwendet automatisch GPU-Instanzierung, um die CPU-Überlastung und Draw-Calls zu reduzieren, indem ähnliche GameObjects zusammengefasst werden.
- Dynamisches Batching kombiniert kleine Meshes zur Laufzeit, was ein Vorteil auf älteren mobilen Geräten mit hohen Draw-Call-Kosten sein kann. Der Nachteil ist jedoch, dass die Vertex-Transformation ebenfalls ressourcenintensiv sein kann.
- GPU-Oklusions-Culling verwendet Compute-Shader, um die Sichtbarkeit von Objekten zu bestimmen, indem Tiefenpuffer von aktuellen und vorherigen Frames verglichen werden, wodurch unnötiges Rendern von okkludierten Objekten reduziert wird, ohne dass vorab gebackene Daten erforderlich sind.
Zusätzlich können auf der CPU-Seite Techniken wie Camera.layerCullDistances verwendet werden, um die Anzahl der Objekte, die an den Render-Thread gesendet werden, zu reduzieren, indem Objekte basierend auf ihrer Entfernung von der Kamera gecullt werden, was hilft, CPU-Flaschenhälse während des Kamera-Cullings zu verringern.
Dies sind nur einige der verfügbaren Optionen. Jede dieser Optionen hat unterschiedliche Vorteile und Nachteile. Einige sind auf bestimmte Plattformen beschränkt. Projekte müssen oft eine Kombination mehrerer dieser Systeme verwenden, und um dies zu tun, ist ein Verständnis erforderlich, wie man das Beste aus ihnen herausholt.

CPU-gebunden: Arbeiter-Threads
Projekte, die durch CPU-Threads anders als die Haupt- oder Render-Threads gebunden sind, sind nicht sehr häufig. Es kann jedoch auftreten, wenn Ihr Projekt den Data-Oriented Technology Stack (DOTS) verwendet, insbesondere wenn Arbeiten vom Haupt-Thread in Worker-Threads unter Verwendung des Job-Systems verschoben werden.
Das obige Bild ist eine Aufnahme aus dem Spielmodus im Editor, die ein DOTS-Projekt zeigt, das eine Partikelfluid-Simulation auf der CPU ausführt.
Es sieht auf den ersten Blick nach einem Erfolg aus. Die Worker-Threads sind eng gepackt mit Burst-kompilierten Jobs, was darauf hinweist, dass eine große Menge an Arbeit vom Haupt-Thread verschoben wurde. In der Regel ist dies eine sinnvolle Entscheidung.
In diesem Fall sind jedoch die Frame-Zeit von 48,14 ms und der graue WaitForJobGroupID Marker von 35,57 ms im Haupt-Thread Anzeichen dafür, dass nicht alles gut ist. WaitForJobGroupID zeigt an, dass der Haupt-Thread Jobs geplant hat, die asynchron auf Worker-Threads ausgeführt werden, aber er benötigt die Ergebnisse dieser Jobs, bevor die Worker-Threads mit deren Ausführung fertig sind. Die blauen Profiler-Markierungen unter WaitForJobGroupID zeigen, dass der Haupt-Thread Jobs ausführt, während er wartet, um sicherzustellen, dass die Jobs schneller abgeschlossen werden.
Obwohl die Jobs Burst-kompiliert sind, leisten sie immer noch viel Arbeit. Vielleicht sollte die räumliche Abfrage-Struktur, die von diesem Projekt verwendet wird, um schnell zu finden, welche Partikel nahe beieinander sind, optimiert oder gegen eine effizientere Struktur ausgetauscht werden. Oder die räumlichen Abfrage-Jobs können für das Ende des Frames geplant werden, anstatt für den Anfang, wobei die Ergebnisse erst zu Beginn des nächsten Frames benötigt werden. Vielleicht versucht dieses Projekt, zu viele Partikel zu simulieren. Eine weitere Analyse des Codes der Jobs ist erforderlich, um die Lösung zu finden, daher kann das Hinzufügen von feinkörnigen Profiler-Markern helfen, ihre langsamsten Teile zu identifizieren.
Die Jobs in Ihrem Projekt sind möglicherweise nicht so parallelisiert wie in diesem Beispiel. Vielleicht haben Sie nur einen langen Job, der in einem einzelnen Worker-Thread läuft. Das ist in Ordnung, solange die Zeit zwischen der Planung des Jobs und der Zeit, die er benötigt, um abgeschlossen zu werden, lang genug ist, damit der Job ausgeführt werden kann. Wenn nicht, werden Sie sehen, dass der Haupt-Thread stockt, während er auf den Abschluss des Jobs wartet, wie im obigen Screenshot.
Häufige Fallstricke für Engpässe bei Worker-Threads
Häufige Ursachen für Synchronisationspunkte und Engpässe bei Worker-Threads sind:
- Jobs, die nicht vom Burst-Compiler kompiliert werden
- Lang laufende Jobs in einem einzelnen Worker-Thread, anstatt über mehrere Worker-Threads parallelisiert zu werden
- Unzureichende Zeit zwischen dem Punkt im Frame, an dem ein Job geplant wird, und dem Punkt, an dem das Ergebnis benötigt wird
- Mehrere "Synchronisationspunkte" in einem Frame, die erfordern, dass alle Jobs sofort abgeschlossen werden
Sie können die Flow Events Funktion in der Timeline-Ansicht des CPU Usage Profiler-Moduls verwenden, um zu untersuchen, wann Jobs geplant werden und wann ihre Ergebnisse vom Haupt-Thread erwartet werden.
Für weitere Informationen zum Schreiben effizienter DOTS-Codes siehe den DOTS Best Practices Leitfaden.

GPU-abhängig
Ihre Anwendung ist GPU-gebunden, wenn der Haupt-Thread viel Zeit in Profiler-Markern wie Gfx.WaitForPresentOnGfxThread verbringt und Ihr Render-Thread gleichzeitig Marker wie Gfx.PresentFrame oder .WaitForLastPresent. anzeigt.
Der beste Weg, um GPU-Frame-Zeiten zu erhalten, ist die Verwendung eines plattformspezifischen GPU-Profiling-Tools, aber nicht alle Geräte machen es einfach, zuverlässige Daten zu erfassen.
Die FrameTimingManager API kann in diesen Fällen hilfreich sein, da sie sowohl auf der CPU als auch auf der GPU niedrige Overhead-, hochgradige Frame-Zeiten bereitstellt.
Die obige Aufnahme wurde mit einem Android-Mobiltelefon unter Verwendung der Vulkan-Grafik-API gemacht. Obwohl ein Teil der Zeit, die in Gfx.PresentFrame in diesem Beispiel verbracht wird, möglicherweise mit dem Warten auf VSync zusammenhängt, zeigt die extreme Länge dieses Profiler-Markers, dass der Großteil dieser Zeit damit verbracht wird, auf die GPU zu warten, um das vorherige Bild zu rendern.
In diesem Spiel haben bestimmte Gameplay-Ereignisse die Verwendung eines Shaders ausgelöst, der die Anzahl der von der GPU gerenderten Draw-Calls verdreifachte. Häufige Probleme, die bei der Profilerstellung der GPU-Leistung untersucht werden sollten, sind:
- Teure Vollbild-Post-Processing-Effekte wie Ambient Occlusion und Bloom
- Teure Fragment-Shader verursacht durch:
- Verzweigungslogik im Shader-Code
- Verwendung der vollen Float-Präzision anstelle der halben Präzision, insbesondere auf Mobilgeräten
- Übermäßige Verwendung von Registern, die die Wellenfrontbelegung von GPUs beeinflussen
- Überzeichnung in der transparenten Render-Warteschlange verursacht durch:
- Ineffizientes UI-Rendering
- Überlappende oder übermäßige Verwendung von Partikelsystemen
- Post-Processing-Effekte
- Übermäßig hohe Bildschirmauflösungen, wie:
- 4K-Displays
- Retina-Displays auf mobilen Geräten
- Mikro-Dreiecke verursacht durch:
- Dichte Mesh-Geometrie
- Mangel an Level of Detail (LOD)-Systemen, was ein besonderes Problem auf mobilen GPUs darstellt, aber auch PC- und Konsolen-GPUs betreffen kann
- Cache-Fehlermeldungen und verschwendete GPU-Speicherbandbreite verursacht durch:
- Unkomprimierte Texturen
- Hochauflösende Texturen ohne Mipmaps
- Geometrie- oder Tessellation-Shader, die möglicherweise mehrmals pro Frame ausgeführt werden, wenn dynamische Schatten aktiviert sind
Wenn Ihre Anwendung GPU-gebunden zu sein scheint, können Sie den Frame-Debugger als schnellen Weg verwenden, um die Draw-Call-Batches zu verstehen, die an die GPU gesendet werden. Dieses Tool kann jedoch keine spezifischen GPU-Zeitinformationen bereitstellen, nur wie die gesamte Szene aufgebaut ist.
Der beste Weg, um die Ursache von GPU-Engpässen zu untersuchen, besteht darin, einen GPU-Capture von einem geeigneten GPU-Profiling-Tool zu überprüfen. Welches Tool Sie verwenden, hängt von der Zielhardware und der gewählten Grafik-API ab. Siehe den Abschnitt über Profilierungs- und Debugging-Tools im E-Book für weitere Informationen.

Finden Sie weitere Best Practices und Tipps im Unity Best Practices Hub. Wählen Sie aus über 30 Leitfäden, die von Branchenexperten sowie Unity-Ingenieuren und technischen Künstlern erstellt wurden, die Ihnen helfen, effizient mit den Werkzeugen und Systemen von Unity zu entwickeln.