Eine reibungslose Leistung ist unerlässlich, um den Spielern ein intensives Spielerlebnis zu bieten. Indem Sie die Leistung Ihres Spiels für eine breite Palette von Plattformen und Geräten profilieren und optimieren, können Sie Ihre Spielerbasis erweitern und Ihre Erfolgschancen erhöhen.
Auf dieser Seite wird ein allgemeiner Profiling-Workflow für Spieleentwickler beschrieben. Es ist ein Auszug aus dem E-Book " Ultimate guide to profiling Unity games", das kostenlos heruntergeladen werden kann. Das E-Book wurde von externen und internen Unity-Experten für Spielentwicklung, Profiling und Optimierung erstellt.
Lesen Sie weiter, um mehr über nützliche Ziele zu erfahren, die Sie sich mit der Profilerstellung setzen können, über häufige Leistungsengpässe, wie z. B. CPU- oder GPU-Bindung, und darüber, wie Sie diese Situationen im Detail identifizieren und untersuchen können.
Die Messung der Framerate Ihres Spiels in Bildern pro Sekunde (fps) ist nicht ideal, um Ihren Spielern ein konsistentes Erlebnis zu bieten. Betrachten Sie das folgende vereinfachte Szenario:
Während der Laufzeit rendert Ihr Spiel 59 Bilder in 0,75 Sekunden. Das nächste Bild braucht jedoch 0,25 Sekunden zum Rendern. Die durchschnittliche Bildrate von 60 Bildern pro Sekunde hört sich gut an, aber in der Realität werden die Spieler einen Stottereffekt feststellen, da das letzte Bild eine Viertelsekunde zum Rendern benötigt.
Dies ist einer der Gründe, warum es wichtig ist, sich ein bestimmtes Zeitbudget pro Frame zu setzen. So haben Sie ein solides Ziel, auf das Sie bei der Erstellung von Profilen und der Optimierung Ihres Spiels hinarbeiten können, und letztlich wird dadurch ein reibungsloseres und konsistenteres Spielerlebnis geschaffen.
Jedes Bild hat ein Zeitbudget, das sich nach den angestrebten fps richtet. Eine Anwendung, die 30 Bilder pro Sekunde anstrebt, sollte immer weniger als 33,33 ms pro Bild benötigen (1000 ms / 30 Bilder pro Sekunde). Bei einem Zielwert von 60 Bildern pro Sekunde bleiben 16,66 ms pro Bild (1000 ms / 60 Bilder pro Sekunde).
Sie können dieses Budget während nicht interaktiver Sequenzen überschreiten, z. B. bei der Anzeige von Benutzeroberflächenmenüs oder beim Laden von Szenen, aber nicht während des Spiels. Schon ein einziges Bild, das das Zielbildbudget überschreitet, führt zu Störungen.
Hinweis Eine gleichbleibend hohe Bildrate in VR-Spielen ist unerlässlich, um Übelkeit oder Unbehagen beim Spieler zu vermeiden. Ohne sie riskieren Sie, bei der Zertifizierung Ihres Spiels vom Plattforminhaber abgelehnt zu werden.
Bilder pro Sekunde: Eine trügerische Metrik
Eine gängige Methode, mit der Gamer die Leistung messen, ist die Framerate, also die Bilder pro Sekunde. Es wird jedoch empfohlen, stattdessen die Rahmenzeit in Millisekunden zu verwenden. Um zu verstehen, warum das so ist, sehen Sie sich das obige Diagramm an, das die fps mit der Frame-Zeit vergleicht.
Betrachten Sie diese Zahlen:
1000 ms/sec / 900 fps = 1,111 ms pro Bild
1000 ms/sec / 450 fps = 2,222 ms pro Bild
1000 ms/sec / 60 fps = 16,666 ms pro Bild
1000 ms/sec / 56,25 fps = 17,777 ms pro Bild
Wenn Ihre Anwendung mit 900 Bildern pro Sekunde läuft, entspricht dies einer Frame-Zeit von 1,111 Millisekunden pro Frame. Bei 450 fps sind das 2,222 Millisekunden pro Bild. Dies entspricht einem Unterschied von nur 1,111 Millisekunden pro Bild, obwohl die Bildrate um die Hälfte zu sinken scheint.
Betrachtet man die Unterschiede zwischen 60 Bildern pro Sekunde und 56,25 Bildern pro Sekunde, so ergibt sich eine Zeitspanne von 16,666 Millisekunden pro Bild bzw. 17,777 Millisekunden pro Bild. Dies entspricht ebenfalls 1,111 Millisekunden mehr pro Bild, aber hier ist der prozentuale Rückgang der Bildrate weit weniger dramatisch.
Aus diesem Grund verwenden die Entwickler zur Bewertung der Spielgeschwindigkeit die durchschnittliche Bildwiederholzeit und nicht die fps.
Machen Sie sich keine Sorgen über fps, solange Sie nicht unter Ihre Zielbildrate fallen. 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 Dunlop's fps versus frame time", um weitere Informationen zu erhalten.
Die Wärmeregulierung ist einer der wichtigsten Bereiche, der bei der Entwicklung von Anwendungen für mobile Geräte optimiert werden muss. Wenn die CPU oder GPU aufgrund eines ineffizienten Designs zu lange mit voller Leistung arbeiten, werden diese Chips heiß. Um die Chips nicht zu beschädigen (und möglicherweise die Hände des Spielers zu verbrennen!), reduziert das Betriebssystem die Taktfrequenz des Geräts, damit es sich abkühlen kann, was zu stotternden Bildern und einem schlechten Benutzererlebnis führt. Diese Leistungsminderung wird als thermische Drosselung bezeichnet.
Höhere Bildwiederholraten und eine erhöhte Code-Ausführung (oder DRAM-Zugriffe) führen zu einem erhöhten Batterieverbrauch und einer erhöhten Wärmeentwicklung. Eine schlechte Leistung kann auch ganze Segmente von Mobilgeräten des unteren Segments ausschließen, was zu verpassten Marktchancen und damit zu geringeren Umsätzen führen kann.
Wenn Sie sich mit dem Problem der Thermik befassen, sollten Sie das Budget, mit dem Sie arbeiten müssen, als ein systemweites Budget betrachten.
Bekämpfen Sie die thermische Drosselung und den Batterieverbrauch, indem Sie eine frühe Profilierungstechnik nutzen, um Ihr Spiel von Anfang an zu optimieren. Passen Sie die Projekteinstellungen an die Hardware Ihrer Zielplattform an, um Probleme mit der Wärmeentwicklung und dem Batterieverbrauch zu vermeiden.
Rahmenbudgets auf dem Handy anpassen
Eine Leerlaufzeit von ca. 35 % ist die übliche Empfehlung, um thermische Probleme des Geräts bei längerer Spielzeit zu vermeiden. Dies gibt den mobilen Chips Zeit, sich abzukühlen, und hilft, eine übermäßige Entladung der Batterie zu verhindern. Bei einer angestrebten Bildwiederholzeit von 33,33 ms pro Bild (bei 30 Bildern pro Sekunde) liegt das typische Bildbudget für mobile Geräte bei etwa 22 ms pro Bild.
Die Berechnung sieht folgendermaßen aus: (1000 ms / 30) * 0,65 = 21,66 ms
Um auf dem Handy 60 Bilder pro Sekunde zu erreichen, wäre nach derselben Berechnung eine Zielbildzeit von (1000 ms / 60) * 0,65 = 10,83 ms erforderlich. Dies ist auf vielen mobilen Geräten nur schwer zu erreichen und würde den Akku doppelt so schnell entleeren, wie wenn man 30 Bilder pro Sekunde anpeilt. Aus diesem Grund zielen die meisten Handyspiele eher auf 30 als auf 60 fps ab. Verwenden Sie Application.targetFrameRate, um diese Einstellung zu steuern, und lesen Sie den Abschnitt "Festlegen eines Frame-Budgets" im E-Book, um mehr über die Frame-Zeit zu erfahren.
Die Frequenzskalierung auf mobilen Chips kann es schwierig machen, bei der Profilerstellung die Budgetzuweisungen für die Leerlaufzeit von Frames zu ermitteln. Ihre Verbesserungen und Optimierungen können einen positiven Nettoeffekt haben, aber das mobile Gerät könnte die Frequenz herunterskalieren und folglich kühler laufen. Verwenden Sie benutzerdefinierte Tools wie FTrace oder Perfetto, um die Frequenzen mobiler Chips, Leerlaufzeiten und Skalierung vor und nach Optimierungen zu überwachen.
Solange Sie innerhalb Ihres Budgets für die Gesamtzeit der Bildwiederholrate bleiben (33,33 ms für 30 fps) und feststellen, dass Ihr Gerät weniger arbeitet oder geringere Temperaturen aufzeichnet, um diese Bildwiederholrate zu halten, sind Sie auf dem richtigen Weg.
Ein weiterer Grund für die Erweiterung des Rahmenbudgets bei mobilen Geräten ist die Berücksichtigung von Temperaturschwankungen in der Realität. An einem heißen Tag erhitzt sich ein mobiles Gerät und hat Probleme, die Wärme abzuleiten, was zu thermischer Drosselung und schlechter Spielleistung führen kann. Die Festlegung eines bestimmten Prozentsatzes des Rahmenbudgets hilft, diese Art von Szenarien zu vermeiden.
Der DRAM-Zugriff ist in der Regel ein stromfressender Vorgang auf mobilen Geräten. Die Optimierungshinweise von Arm für Grafikinhalte auf mobilen Geräten besagen, dass der LPDDR4-Speicherzugriff etwa 100 Picojoule pro Byte kostet.
Reduzieren Sie die Anzahl der Speicherzugriffe pro Frame um:
- Reduzierung der Bildrate
- Verringerung der Bildschirmauflösung, wo dies möglich ist
- Verwendung einfacherer Netze mit geringerer Scheitelpunktzahl und Attributgenauigkeit
- Texturkomprimierung und Mipmapping verwenden
Wenn Sie sich auf Geräte mit Arm- oder Arm Mali-Hardware konzentrieren müssen, enthält das Arm Mobile Studio-Tooling (insbesondere der Streamline Performance Analyzer) einige großartige Leistungszähler zur Identifizierung von Problemen mit der Speicherbandbreite. Die Zähler werden für jede Arm-GPU-Generation aufgelistet und erläutert, z. B. für die Mali-G78. Beachten Sie, dass die GPU-Profilierung in Mobile Studio Arm Mali erfordert.
Festlegung von Hardware-Ebenen für das Benchmarking
Zusätzlich zur Verwendung plattformspezifischer Profiling-Tools sollten Sie für jede Plattform und Qualitätsstufe, die Sie unterstützen möchten, Stufen oder ein Gerät mit der niedrigsten Spezifikation einrichten und dann für jede dieser Spezifikationen ein Profil erstellen und die Leistung optimieren.
Wenn Sie beispielsweise auf mobile Plattformen abzielen, könnten Sie sich entscheiden, drei Stufen mit Qualitätskontrollen zu unterstützen, die Funktionen je nach Zielhardware ein- oder ausschalten. Sie optimieren dann für die niedrigste Gerätespezifikation in jeder Schicht. Ein weiteres Beispiel: Wenn Sie ein Spiel sowohl für PlayStation 4 als auch für PlayStation 5 entwickeln, stellen Sie sicher, dass Sie für beide Versionen ein Profil erstellen.
Einen vollständigen Leitfaden zur Optimierung für mobile Geräte finden Sie unter Optimieren Sie die Leistung Ihres mobilen Spiels. In diesem E-Book finden Sie viele Tipps und Tricks, die Ihnen helfen, die thermische Drosselung zu reduzieren und die Akkulaufzeit für mobile Geräte, auf denen Ihre Spiele laufen, zu verlängern.
Bei der Profilerstellung ist ein Ansatz von oben nach unten sinnvoll, beginnend mit deaktiviertem Deep Profiling. Verwenden Sie diesen Ansatz auf hoher Ebene, um Daten zu sammeln und Notizen darüber zu machen, welche Szenarien unerwünschte verwaltete Zuweisungen oder zu viel CPU-Zeit in den Kernbereichen Ihrer Spielschleife verursachen.
Sie müssen zunächst die Aufrufstapel für GC.Alloc-Marker sammeln. Wenn Sie mit diesem Prozess nicht vertraut sind, finden Sie einige Tipps und Tricks im Abschnitt "Auffinden wiederkehrender Speicherzuweisungen über die Lebensdauer der Anwendung" in Ultimative Anleitung zum Profiling von Unity-Spielen.
Wenn die gemeldeten Aufrufstapel nicht detailliert genug sind, um die Quelle der Zuweisungen oder anderer Verlangsamungen aufzuspüren, können Sie eine zweite Profilerstellungssitzung mit aktiviertem Deep Profiling durchführen, um die Quelle der Zuweisungen zu finden.
Wenn Sie Notizen zu den "Tätern" der Rahmenzeit sammeln, sollten Sie darauf achten, wie sie sich im Vergleich zum Rest des Rahmens verhalten. Diese relative Auswirkung wird durch die Aktivierung des Deep Profiling beeinflusst.
Lesen Sie mehr über Deep Profiling in Ultimative Anleitung zum Profiling von Unity-Spielen.
Profil früh
Den größten Nutzen aus der Profilerstellung ziehen Sie, wenn Sie bereits zu einem frühen Zeitpunkt im Entwicklungszyklus Ihres Projekts damit beginnen.
Erstellen Sie frühzeitig und häufig ein Profil, damit Sie und Ihr Team eine "Leistungssignatur" für das Projekt verstehen und einprägen können. Wenn die Leistung nachlässt, können Sie leicht erkennen, wenn etwas schief läuft, und das Problem beheben.
Die genauesten Profiling-Ergebnisse erhält man immer, wenn man Builds auf Zielgeräten ausführt und profiliert sowie plattformspezifische Tools einsetzt, um die Hardwareeigenschaften der einzelnen Plattformen zu untersuchen. Diese Kombination bietet Ihnen einen ganzheitlichen Überblick über die Anwendungsleistung auf allen Ihren Zielgeräten.
Laden Sie die druckbare PDF-Version dieser Tabelle hier herunter.
Auf einigen Plattformen ist es einfach festzustellen, ob Ihre Anwendung CPU- oder GPU-gebunden ist. Wenn Sie z. B. ein iOS-Spiel in Xcode ausführen, zeigt das fps-Panel ein Balkendiagramm mit der gesamten CPU- und GPU-Zeit an, damit Sie sehen können, welche die höchste ist. Die CPU-Zeit beinhaltet die Zeit, die für das Warten auf VSync aufgewendet wird, das auf mobilen Geräten immer aktiviert ist.
Auf einigen Plattformen kann es jedoch schwierig sein, GPU-Timing-Daten zu erhalten. Glücklicherweise zeigt der Unity Profiler genügend Informationen an, um den Ort von Leistungsengpässen zu identifizieren. Das obige Flussdiagramm veranschaulicht den anfänglichen Profilierungsprozess; die folgenden Abschnitte enthalten detaillierte Informationen zu den einzelnen Schritten. Sie zeigen auch Profiler-Aufnahmen von echten Unity-Projekten, um zu veranschaulichen, worauf man achten muss.
Um ein ganzheitliches Bild aller CPU-Aktivitäten zu erhalten, einschließlich der Zeit, in der auf die GPU gewartet wird, verwenden Sie die Zeitleistenansicht im CPU-Modul des Profilers. Machen Sie sich mit den gängigen Profiler-Markern vertraut, um Captures richtig zu interpretieren. Einige der Profiler-Marker können je nach Zielplattform unterschiedlich aussehen. Nehmen Sie sich also Zeit, um Captures Ihres Spiels auf jeder Ihrer Zielplattformen zu untersuchen, um ein Gefühl dafür zu bekommen, wie ein "normales" Capture für Ihr Projekt aussieht.
Die Leistung eines Projekts wird durch den Chip und/oder den Thread begrenzt, der am längsten braucht. Das ist der Bereich, auf den Sie sich bei Ihren Optimierungsbemühungen konzentrieren sollten. Stellen Sie sich z. B. ein Spiel mit einer Zielbildzeit von 33,33 ms und aktiviertem VSync vor:
- Wenn die CPU-Framezeit (ohne VSync) 25 ms und die GPU-Zeit 20 ms beträgt, ist das kein Problem! Sie sind CPU-gebunden, aber alles ist im Rahmen des Budgets, und die Optimierung von Dingen wird die Bildrate nicht verbessern (es sei denn, Sie bekommen sowohl CPU als auch GPU unter 16,66 ms und springen auf 60 fps).
- Wenn die CPU-Framezeit 40 ms und die GPU 20 ms beträgt, sind Sie an die CPU gebunden und müssen die CPU-Leistung optimieren. Die Optimierung der GPU-Leistung wird nicht helfen. Vielmehr sollten Sie einen Teil der CPU-Arbeit auf die GPU verlagern, z. B. durch die Verwendung von Compute-Shadern anstelle von C#-Code für bestimmte Aufgaben, um einen Ausgleich zu schaffen.
- Wenn die CPU-Framezeit 20 ms und die GPU 40 ms beträgt, sind Sie an die GPU gebunden und müssen die GPU-Arbeit optimieren.
- Wenn CPU und GPU beide auf 40 ms stehen, sind Sie an beide gebunden und müssen beide unter 33,33 ms optimieren, um 30 fps zu erreichen.
Diese Ressourcen befassen sich näher mit der Frage, ob man CPU- oder GPU-gebunden ist:
Durch frühzeitiges und häufiges Profiling und Optimieren Ihres Projekts während der Entwicklung können Sie sicherstellen, dass alle CPU-Threads Ihrer Anwendung und die gesamte GPU-Frame-Zeit innerhalb des Frame-Budgets liegen.
Oben sehen Sie ein Bild eines Profiler-Captures eines Unity-Mobilspiels, das von einem Team entwickelt wurde, das fortlaufend Profilerstellung und Optimierung durchführte. Das Spiel erreicht 60 fps auf hochauflösenden Mobiltelefonen und 30 fps auf Handys mit mittlerer/geringer Auflösung, wie dem hier abgebildeten.
Beachten Sie, dass fast die Hälfte der Zeit auf dem ausgewählten Frame von der gelben WaitForTargetfps-Profilermarkierung belegt wird. Die Anwendung hat Application.targetFrameRate auf 30 fps gesetzt, und VSync ist aktiviert. Die eigentliche Verarbeitungsarbeit des Haupt-Threads endet etwa bei der 19 ms-Marke, und die restliche Zeit wird damit verbracht, die verbleibenden 33,33 ms abzuwarten, bevor das nächste Bild beginnt. Obwohl diese Zeit mit einem Profiler-Marker dargestellt wird, ist der Haupt-CPU-Thread während dieser Zeit im Wesentlichen inaktiv, so dass die CPU abkühlen kann und nur ein Minimum an Batteriestrom verbraucht wird.
Die Markierung, auf die Sie achten müssen, kann auf anderen Plattformen anders sein oder wenn VSync deaktiviert ist. Wichtig ist, dass Sie überprüfen, ob der Hauptthread innerhalb Ihres Frame-Budgets oder genau auf Ihrem Frame-Budget läuft, mit einer Art Markierung, die anzeigt, dass die Anwendung auf VSync wartet und ob die anderen Threads Leerlaufzeit haben.
Die Leerlaufzeit wird durch graue oder gelbe Profiler-Markierungen dargestellt. Der obige Screenshot zeigt, dass der Render-Thread in Gfx.WaitForGfxCommandsFromMainThread im Leerlauf ist. Dies zeigt an, dass er in einem Frame keine Zeichenaufrufe mehr an die GPU gesendet hat und im nächsten Frame auf weitere Zeichenaufrufe von der CPU wartet. Ähnlich verhält es sich mit dem Thread Job Worker 0, der zwar einige Zeit in Canvas.GeometryJob verbringt, sich aber die meiste Zeit im Leerlauf befindet. Dies sind alles Anzeichen für eine Anwendung, die sich bequem im Rahmen des Budgets bewegt.
Wenn Ihr Spiel im Rahmen des Budgets liegt
Wenn Sie sich innerhalb des Rahmenbudgets befinden, einschließlich aller Anpassungen, die aufgrund von Akkuverbrauch und thermischer Drosselung vorgenommen wurden, haben Sie die Leistungsprofilerstellung bis zum nächsten Mal abgeschlossen - herzlichen Glückwunsch. Führen Sie den Memory Profiler aus, um sicherzustellen, dass die Anwendung auch innerhalb ihres Speicherbudgets bleibt.
Das Bild oben zeigt ein Spiel, das bequem innerhalb des für 30 fps erforderlichen Frame-Budgets von ~22 ms läuft. Beachten Sie die WaitForTargetfps, die die Zeit des Hauptthreads bis zum VSync auffüllen, und die grauen Leerlaufzeiten im Render- und Worker-Thread. Beachten Sie auch, dass das VBlank-Intervall beobachtet werden kann, indem Sie die Endzeiten von Gfx.Present Frame für Frame betrachten. Sie können eine Zeitskala im Zeitleistenbereich oder auf dem Zeitlineal oben erstellen, um von einem dieser Intervalle zum nächsten zu messen.
Wenn Ihr Spiel nicht innerhalb des CPU-Frame-Budgets liegt, ist der nächste Schritt, zu untersuchen, welcher Teil der CPU der Engpass ist - mit anderen Worten, welcher Thread am meisten beschäftigt ist. Wenn Sie sich auf Vermutungen verlassen, kann es passieren, dass Sie Teile des Spiels optimieren, die keine Engpässe darstellen, was zu einer geringen oder gar keiner Verbesserung der Gesamtleistung führt. Einige "Optimierungen" können die Gesamtleistung Ihres Spiels sogar verschlechtern.
Es ist selten, dass die gesamte CPU-Arbeitslast den Engpass darstellt. Moderne CPUs haben eine Reihe verschiedener Kerne, die unabhängig voneinander und gleichzeitig arbeiten können. Auf jedem CPU-Kern können verschiedene Threads laufen. Eine vollständige Unity-Anwendung verwendet eine Reihe von Threads für verschiedene Zwecke, aber die Threads, bei denen am häufigsten Leistungsprobleme festgestellt werden, sind:
- Das Hauptthema: Hier wird standardmäßig die gesamte Spiellogik bzw. die Skripte ausgeführt und die meiste Zeit wird für Funktionen und Systeme wie Physik, Animation, Benutzeroberfläche und Rendering aufgewendet.
- Der Renderfaden: Während des Rendering-Prozesses untersucht der Haupt-Thread die Szene und führt Camera Culling, Depth Sorting und Draw Call Batching durch, was zu einer Liste der zu rendernden Objekte führt. Diese Liste wird an den Rendering-Thread weitergegeben, der sie von der internen plattformunabhängigen Darstellung von Unity in die spezifischen Grafik-API-Aufrufe übersetzt, die erforderlich sind, um die GPU auf einer bestimmten Plattform zu instruieren.
- Die Jobworker-Threads: Entwickler können das C# Job System nutzen, um bestimmte Arbeiten auf Worker-Threads laufen zu lassen, was die Arbeitslast auf dem Hauptthread reduziert. Einige der Systeme und Funktionen von Unity nutzen ebenfalls das Job-System, z. B. Physik, Animation und Rendering.
Hauptthema
Das obige Bild zeigt, wie die Dinge in einem Projekt aussehen könnten, das an den Hauptthread gebunden ist. Dieses Projekt läuft auf einem Meta Quest 2, der normalerweise ein Frame-Budget von 13,88 ms (72 fps) oder sogar 8,33 ms (120 fps) anstrebt, da hohe Frame-Raten wichtig sind, um Motion Sickness in VR-Geräten zu vermeiden. Aber selbst wenn dieses Spiel auf 30 fps abzielen würde, ist es klar, dass dieses Projekt in Schwierigkeiten steckt.
Obwohl der Rendering-Thread und die Worker-Threads ähnlich aussehen wie in dem Beispiel, das innerhalb des Frame-Budgets liegt, ist der Haupt-Thread während des gesamten Frames eindeutig mit Arbeit beschäftigt. Selbst wenn man den geringen Profiler-Overhead am Ende des Frames berücksichtigt, ist der Hauptthread über 45 ms lang beschäftigt, was bedeutet, dass dieses Projekt Frame-Raten von weniger als 22 fps erreicht. Es gibt keine Markierung, die anzeigt, dass der Haupt-Thread untätig auf VSync wartet; er ist den ganzen Frame lang beschäftigt.
In der nächsten Phase der Untersuchung geht es darum, die Teile des Rahmens zu ermitteln, die am längsten brauchen, und zu verstehen, warum dies so ist. Bei diesem Frame benötigt PostLateUpdate.FinishFrameRendering 16,23 ms, mehr als das gesamte Frame-Budget. Bei näherer Betrachtung erkennt man, 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, ist die Reduzierung der Anzahl aktiver Kameras, idealerweise auf nur eine, die vorrangigste Aufgabe für dieses Projekt.
BehaviourUpdate, die Markierung, die alle MonoBehaviour Update()-Methoden umfasst, benötigt 7,27 ms, und die magentafarbenen Abschnitte der Zeitleiste zeigen an, wo Skripte verwalteten Heap-Speicher zuweisen. Der Wechsel zur Hierarchieansicht und die Filterung durch Eingabe von GC.Alloc in der Suchleiste zeigt, dass die Zuweisung dieses Speichers in diesem Frame etwa 0,33 ms dauert. Dies ist jedoch eine ungenaue Messung der Auswirkungen, die die Speicherzuweisungen auf die CPU-Leistung haben.
GC.Alloc-Marker werden nicht tatsächlich durch die Messung der Zeit von einem Begin- zu einem Endpunkt gemessen. Um ihren Overhead gering zu halten, werden sie nur mit ihrem Begin-Zeitstempel und der Größe ihrer Zuweisung erfasst. Der Profiler ordnet ihnen eine minimale Zeitspanne zu, um sicherzustellen, dass sie sichtbar sind. Die eigentliche Zuweisung kann länger dauern, insbesondere wenn ein neuer Speicherbereich vom System angefordert werden muss. Um die Auswirkungen deutlicher zu sehen, platzieren Sie Profiler-Markierungen um den Code, der die Zuweisung vornimmt. Beim Deep Profiling geben die Lücken zwischen den magentafarbenen GC.Alloc-Beispielen in der Zeitleistenansicht einen Hinweis darauf, wie lange sie möglicherweise gedauert haben.
Außerdem kann die Zuweisung neuen Speichers negative Auswirkungen auf die Leistung haben, die schwerer zu messen und direkt zuzuordnen sind:
- Die Anforderung neuen Speichers durch das System kann sich auf das Energiebudget eines mobilen Geräts auswirken, 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 dadurch die vorhandenen Cache-Zeilen.
- Inkrementelle oder synchrone Garbage Collection kann direkt oder verzögert ausgelöst werden, wenn der vorhandene freie Speicherplatz im Managed Memory irgendwann überschritten wird.
Zu Beginn des Bildes summieren sich die vier Instanzen von Physics.FixedUpdate auf 4,57 ms. Später dauert LateBehaviourUpdate (Aufrufe von MonoBehaviour.LateUpdate()) 4 ms, und Animatoren benötigen etwa 1 ms.
Um sicherzustellen, dass dieses Projekt das gewünschte Frame-Budget und die gewünschte Rate erreicht, müssen alle diese Hauptthemen untersucht werden, um geeignete Optimierungen zu finden. Die größten Leistungssteigerungen lassen sich erzielen, wenn die Dinge optimiert werden, die am längsten dauern.
Die folgenden Bereiche sind oft ergiebige Ansatzpunkte für die Optimierung von Projekten, die an einen Hauptstrang gebunden sind:
- Physik
- Aktualisierungen des MonoBehaviour-Skripts
- Müllverteilung und/oder -sammlung
- Kameraauslese und Rendering
- Schlechte Stapelung von Auslosungsaufrufen
- UI-Updates, Layouts und Umgestaltungen
- Animation
Je nach dem Thema, das Sie untersuchen möchten, können auch andere Tools hilfreich sein:
- Bei MonoBehaviour-Skripten, die viel Zeit in Anspruch nehmen, aber nicht genau zeigen, warum das der Fall ist, fügen Sie Profiler-Marker zum Code hinzu oder versuchen Sie Deep-Profiling, um den gesamten Aufrufstapel zu sehen.
- Aktivieren Sie für Skripte, die verwalteten Speicher zuweisen, Zuweisungsaufrufstapel, um genau zu sehen, woher die Zuweisungen kommen. Alternativ können Sie das Deep Profiling aktivieren oder den Project Auditor verwenden, der Codeprobleme gefiltert nach Speicher anzeigt, so dass Sie alle Codezeilen identifizieren können, die zu verwalteten Zuweisungen führen.
- Verwenden Sie den Frame-Debugger, um die Ursachen für eine schlechte Stapelung von Zeichnungsaufrufen zu untersuchen.
Umfassende Tipps zur Optimierung Ihres Spiels finden Sie in diesen kostenlosen Unity-Expertenhandbüchern:
- Optimieren der Mobilspiel-Performance
- Optimieren Sie Ihre Spielleistung für Konsole und PC
Der obige Screenshot zeigt ein Projekt, das durch seinen Render-Thread gebunden ist. Es handelt sich um ein Konsolenspiel mit isometrischem Blickwinkel und einem Ziel-Frame-Budget von 33,33 ms.
Die Profiler-Aufnahme zeigt, dass der Haupt-Thread auf den Render-Thread wartet, bevor das Rendering für das aktuelle Bild beginnen kann, wie durch den Gfx.WaitForPresentOnGfxThreadmarker angezeigt. Der Render-Thread sendet immer noch Zeichenaufrufe vom vorherigen Frame und ist nicht bereit, neue Zeichenaufrufe vom Haupt-Thread zu akzeptieren; der Render-Thread verbringt Zeit in Camera.Render.
Sie können zwischen Markierungen, die sich auf das aktuelle Bild beziehen, und Markierungen aus anderen Bildern unterscheiden, da letztere dunkler erscheinen. Sie können auch sehen, dass, sobald der Haupt-Thread in der Lage ist, fortzufahren und Zeichnungsaufrufe für den Render-Thread zu verarbeiten, der Render-Thread über 100 ms benötigt, um das aktuelle Bild zu verarbeiten, was auch einen Engpass während des nächsten Bildes verursacht.
Weitere Untersuchungen ergaben, dass dieses Spiel ein komplexes Rendering-Setup mit neun verschiedenen Kameras und vielen zusätzlichen Durchläufen aufgrund von Ersatz-Shadern hatte. Das Spiel renderte auch über 130 Punktlichter mit einem Vorwärts-Rendering-Pfad, der mehrere zusätzliche transparente Zeichenaufrufe für jedes Licht hinzufügen kann. Insgesamt führten diese Probleme zu über 3000 Aufrufen pro Frame.
Im Folgenden sind die häufigsten Ursachen für Projekte aufgeführt, die thread-gebunden sind:
- Schlechte Stapelverarbeitung von Zeichenaufrufen, insbesondere bei älteren Grafik-APIs wie OpenGL oder DirectX 11
- Zu viele Kameras. Wenn Sie nicht gerade ein Multiplayer-Spiel mit geteiltem Bildschirm entwickeln, sollten Sie immer nur eine aktive Kamera haben.
- Schlechte Auswahl, was dazu führt, dass zu viele Dinge gezeichnet werden. Untersuchen Sie die Kegelstumpfabmessungen Ihrer Kamera und ziehen Sie Ebenenmasken ab. Erwägen Sie die Aktivierung der Okklusionsunterdrückung. Vielleicht erstellen Sie sogar Ihr eigenes einfaches System zur Beseitigung von Verdeckungen auf der Grundlage dessen, was Sie über die Anordnung der Objekte in Ihrer Welt wissen. Schauen Sie sich an, wie viele schattenwerfende Objekte es in der Szene gibt - die Beseitigung von Schatten erfolgt in einem separaten Durchgang zur "normalen" Beseitigung.
Das Rendering-Profiler-Modul zeigt eine Übersicht über die Anzahl der Zeichnungsaufrufe und SetPass-Aufrufe pro Frame. Das beste Werkzeug, um herauszufinden, welche Zeichnungsaufrufe Ihr Render-Thread an die GPU sendet, ist der Frame Debugger.
Projekte, die durch andere CPU-Threads als den Haupt- oder Render-Thread gebunden sind, sind nicht sehr häufig. Es kann jedoch auftreten, wenn Ihr Projekt den datenorientierten Technologiestapel (DOTS) verwendet, insbesondere wenn die Arbeit vom Haupt-Thread in Worker-Threads unter Verwendung des C# Job Systems verschoben wird.
Die obige Aufnahme stammt aus dem Wiedergabemodus des Editors und zeigt ein DOTS-Projekt mit einer Partikel-Fluid-Simulation auf der CPU.
Auf den ersten Blick sieht es nach einem Erfolg aus. Die Worker-Threads sind dicht mit Burst-kompilierten Aufträgen gefüllt, was darauf hindeutet, dass eine große Menge an Arbeit vom Haupt-Thread ausgelagert wurde. In der Regel ist dies eine gute Entscheidung.
In diesem Fall sind jedoch die Rahmenzeit von 48,14 ms und die graue WaitForJobGroupID-Markierung von 35,57 ms auf dem Hauptthread Anzeichen dafür, dass nicht alles in Ordnung ist. WaitForJobGroupID zeigt an, dass der Hauptthread Aufträge geplant hat, die asynchron auf Worker-Threads ausgeführt werden sollen, er aber die Ergebnisse dieser Aufträge benötigt, bevor die Worker-Threads ihre Ausführung beendet haben. Die blauen Profiler-Markierungen unter WaitForJobGroupID zeigen an, dass der Hauptthread Aufträge ausführt, während er wartet, um sicherzustellen, dass die Aufträge früher beendet werden.
Obwohl die Aufträge mit Burst kompiliert sind, machen sie immer noch eine Menge Arbeit. Vielleicht sollte die räumliche Abfragestruktur, die in diesem Projekt verwendet wird, um schnell herauszufinden, welche Partikel nahe beieinander liegen, optimiert oder durch eine effizientere Struktur ersetzt werden. Oder die räumlichen Abfrageaufträge können für das Ende des Bildes und nicht für den Beginn geplant werden, wobei die Ergebnisse erst zu Beginn des nächsten Bildes benötigt werden. Vielleicht wird bei diesem Projekt versucht, zu viele Teilchen zu simulieren. Um die Lösung zu finden, ist eine weitere Analyse des Auftragscodes erforderlich, so dass das Hinzufügen von feineren Profiler-Markierungen helfen kann, die langsamsten Teile zu identifizieren.
Die Aufträge in Ihrem Projekt sind möglicherweise nicht so parallelisiert wie in diesem Beispiel. Vielleicht haben Sie nur einen langen Auftrag, der in einem einzigen Worker-Thread läuft. Das ist in Ordnung, solange die Zeit zwischen dem geplanten Auftrag und dem Zeitpunkt, zu dem er abgeschlossen werden muss, lang genug ist, um den Auftrag auszuführen. Wenn dies nicht der Fall ist, wird der Haupt-Thread blockiert, während er auf die Beendigung des Auftrags wartet, wie im obigen Screenshot zu sehen ist.
Häufige Ursachen für Sync-Points und Worker-Thread-Engpässe sind:
- Aufträge, die vom Burst-Compiler nicht kompiliert werden
- Langlaufende Aufträge auf einem einzigen Worker-Thread statt Parallelisierung auf mehrere Worker-Threads
- Unzureichende Zeit zwischen dem Zeitpunkt, an dem ein Auftrag geplant wird, und dem Zeitpunkt, an dem das Ergebnis benötigt wird
- Mehrere "Synchronisationspunkte" in einem Rahmen, bei denen alle Aufträge sofort abgeschlossen werden müssen
Sie können die Funktion " Ablaufereignisse" in der Ansicht "Zeitleiste" des Moduls "CPU-Nutzungsprofil" verwenden, um zu untersuchen, wann Aufträge geplant werden und wann ihre Ergebnisse vom Hauptthread erwartet werden. Weitere Informationen zum Schreiben von effizientem DOTS-Code finden Sie in der DOTS-Best-Practices Leitfaden.
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 <GraphicsAPIName>.WaitForLastPresent anzeigt.
Die folgende Aufnahme wurde mit einem Samsung Galaxy S7 unter Verwendung der Vulkan-Grafik-API gemacht. Obwohl ein Teil der in Gfx.PresentFrame verbrachten Zeit in diesem Beispiel mit dem Warten auf VSync zusammenhängen könnte, deutet die extreme Länge dieser Profiler-Markierung darauf hin, dass der Großteil dieser Zeit damit verbracht wird, darauf zu warten, dass die GPU das Rendering des vorherigen Frames beendet.
In diesem Spiel lösten bestimmte Spielereignisse die Verwendung eines Shaders aus, der die Anzahl der von der GPU gerenderten Zeichenaufrufe verdreifachte. Bei der Erstellung von Profilen für die GPU-Leistung sind häufig folgende Fragen zu klären:
- Teure bildschirmfüllende Nachbearbeitungseffekte, einschließlich gängiger Übeltäter wie Ambient Occlusion und Bloom
- Teure Fragment-Shader verursacht durch:
- Verzweigungslogik
- Verwendung der vollen Float-Genauigkeit anstelle der halben Genauigkeit
- Übermäßiger Gebrauch von Registern, die die Wellenfrontbelegung von GPUs beeinflussen
- Überzeichnung in der Transparent-Rendering-Warteschlange, verursacht durch ineffiziente UI, Partikelsysteme oder Nachbearbeitungseffekte
- Übermäßig hohe Bildschirmauflösungen, wie sie bei 4K-Displays oder Retina-Displays auf Mobilgeräten zu finden sind
- Mikrodreiecke, die durch dichte Netzgeometrie oder einen Mangel an LODs verursacht werden. Dies ist ein besonderes Problem bei mobilen Grafikprozessoren, kann aber auch bei PC- und Konsolen-GPUs auftreten.
- Cache-Misses und verschwendete GPU-Speicherbandbreite durch unkomprimierte Texturen oder hochauflösende Texturen ohne Mipmaps
- Geometrie- oder Tesselationsshader, die mehrmals pro Bild ausgeführt werden können, wenn dynamische Schatten aktiviert sind
Wenn Ihre Anwendung GPU-gebunden zu sein scheint, können Sie den Frame Debugger verwenden, um die Zeichnungsaufrufe, die an die GPU gesendet werden, schnell zu verstehen. Dieses Tool kann jedoch keine spezifischen GPU-Timing-Informationen anzeigen, sondern nur, wie die Szene insgesamt aufgebaut ist.
Die beste Möglichkeit, die Ursache von GPU-Engpässen zu ermitteln, ist die Untersuchung einer GPU-Aufzeichnung mit einem geeigneten GPU-Profiler. Welches Werkzeug Sie verwenden, hängt von der Zielhardware und der gewählten Grafik-API ab.
Laden Sie das E-Book Ultimate guide to profiling Unity gameskostenlos herunter, um alle Tipps und Best Practices zu erhalten.