Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Generelle Optimierungs-Tipps

Einführung

In einer idealen Welt würden Computer mit unendlicher Geschwindigkeit laufen, und die einzige Grenze für das, was wir erreichen könnten, wäre unsere Vorstellungskraft. In der realen Welt ist es jedoch allzu einfach, Software zu erstellen, die selbst den schnellsten Computer in die Knie zwingt.

Das Entwerfen von Spielen und anderer Software ist somit ein Kompromiss zwischen dem, was wir möchten, und dem, was wir realistisch erreichen können, und dabei eine zufriedenstellende Performance beizubehalten.

Um die besten Ergebnisse zu erzielen, haben wir zwei Ansätze:

  • Schneller arbeiten.

  • Intelligenter arbeiten.

Und idealerweise verwenden wir eine Mischung aus beiden.

Schall und Rauch

Zu einer intelligenteren Arbeitsweise gehört auch die Erkenntnis, dass wir in Spielen den Spieler oft glauben machen können, er befände sich in einer Welt, die weitaus komplexer, interaktiver und grafisch aufregender ist, als sie tatsächlich ist. Ein guter Programmierer ist ein Zauberer und sollte sich bemühen, die Tricks seines Handwerks zu lernen, während er versucht, neue zu erfinden.

Die Natur der Langsamkeit

Für den externen Beobachter werden Performanceprobleme häufig über einen Kamm geschoren. In Wirklichkeit gibt es jedoch verschiedene Arten von Performanceproblemen zu differenzieren:

  • Ein langsamer Prozess, der bei jedem Frame auftritt und zu einer kontinuierlich niedrigen Framerate führt.

  • Ein zeitweise einsetzender Prozess, der Überlastungsspitzen verursacht und damit Verzögerungen hervorruft.

  • Ein langwieriger Prozess, der außerhalb des normalen Spielablaufs stattfindet, z.B. beim Laden eines Levels.

Jedes davon ist für den Benutzer ärgerlich, aber auf unterschiedliche Weise.

Messen der Performance

Das wahrscheinlich wichtigste Instrument zur Optimierung ist die Möglichkeit, die Performance zu messen - um festzustellen, wo Bottlenecks liegen, und um den Erfolg unserer Versuche zu messen, sie zu beschleunigen.

Es gibt mehrere Optionen zur Performancemessung, wie:

Beachten Sie insbesondere, dass die relative Performance der verschiedenen Bereiche auf unterschiedlicher Hardware variieren kann. Es ist oft eine gute Idee, die Laufzeiten auf mehr als einem Gerät zu messen. Dies gilt insbesondere, wenn Sie für mobile Geräte entwickeln.

Einschränkungen

CPU-Profiler sind oft das Mittel der Wahl zur Performancemessung. Sie geben jedoch nicht zwangsläufig Rückschlüsse auf alle Probleme.

  • Bottlenecks treten häufig auf der GPU auf, und zwar "als Folge" der von der CPU erteilten Anweisungen.

  • Spitzen können in den Betriebssystemprozessen (außerhalb von Godot) "als Ergebnis" von Anweisungen auftreten, die in Godot verwendet werden (z.B. dynamische Speicherallokation).

  • Aufgrund der erforderlichen initialen Einrichtung ist es nicht immer möglich, Profiling für bestimmte Geräte wie ein Mobiltelefon durchzuführen.

  • Möglicherweise müssen Performanceprobleme gelöst werden, die auf Hardware auftreten, auf die Sie keinen Zugriff haben.

Aufgrund dieser Einschränkungen müssen Sie oft Detektivarbeit leisten, um herauszufinden, wo die Bottlenecks liegen.

Detektivarbeit

Detektivarbeit ist eine entscheidende Fähigkeit für Entwickler (sowohl in Bezug auf die Performance als auch in Bezug auf das Beheben von Bugs). Dies kann Hypothesentests und Binärsuche umfassen.

Hypothesentests

Nehmen wir zum Beispiel an, Sie glauben, dass Sprites Ihr Spiel verlangsamen. Sie können diese Hypothese testen, indem Sie:

  • die Performance messen, indem Sie weitere Sprites hinzufügen oder einige entfernen.

Dies kann zu einer weiteren Hypothese führen: Bestimmt die Größe des Sprites den Performanceverlust?

  • Sie können dies testen, indem Sie alles gleich lassen, aber die Sprite-Größe ändern, und die Performance messen.

Profiler

Mit Profilern können Sie die Laufzeit Ihres Programms messen. Profiler liefern dann Ergebnisse, die Ihnen sagen, wie viel Prozent der Zeit in verschiedenen Funktionen und Bereichen verbracht wurde und wie oft Funktionen aufgerufen wurden.

Das kann sehr nützlich sein, um Bottlenecks zu identifizieren und die Ergebnisse von Optimierungsansätzen zu beobachten. Manche Ansätze können sich als suboptimal herausstellen und letzlichen zu einer noch schwächeren Performance führen. Verwenden Sie immer Profiling und Timing, um zielgerichtet vorzugehen.

Weitere Informationen über die Verwendung des in Godot eingebauten Profilers finden Sie unter Der Profiler.

Prinzipien

Donald Knuth sagte einmal:

Programmierer verschwenden enorm viel Zeit damit, über die Geschwindigkeit unkritischer Teile ihrer Programme nachzudenken bzw. sich darüber Gedanken zu machen, und diese Versuche der Effizienzsteigerung wirken sich tatsächlich stark negativ aus, wenn Debugging und Wartung in Betracht gezogen werden. Wir sollten kleine Verbesserungen vergessen, etwa in 97% der Fälle: Vorzeitige Optimierung ist die Wurzel allen Übels. Dennoch sollten wir unsere Gelegenheiten bei diesen kritischen 3% nicht verpassen.

Die Botschaften hier sind sehr wichtig:

  • Entwicklungszeit ist begrenzt. Anstatt blindlings zu versuchen, jeden noch so kleinen Aspekt eines Programms zu beschleunigen, sollten wir uns auf diejenen Aspekte konzentrieren, die wirklich wichtig sind.

  • Optimierungsversuche führen häufig zu Code, der schwieriger zu lesen und zu debuggen ist als nicht optimierter Code. Es liegt im eigenen Interesse, dies auf Bereiche zu beschränken, die wesentlich davon profitieren.

Nur weil wir einen bestimmten Teil des Codes optimieren können, heißt das nicht unbedingt, dass wir das auch tun sollten. Zu wissen, wann man optimieren sollte und wann nicht, ist eine wichtige Fähigkeit.

Ein irreführender Aspekt des Zitats ist, dass sich Leute eher auf das Unterzitat "vorzeitige Optimierung ist die Wurzel allen Übels" fokussieren. Während vorzeitige Optimierung (per Definition) unerwünscht ist, ist performante Software das Ergebnis eines performanten Designs.

Performantes Design

Die Gefahr bei der Empfehlung, Optimierungen so lange zu ignorieren, bis sie notwendig sind, besteht darin, dass dabei übersehen wird, dass der wichtigste Zeitpunkt für die Berücksichtigung der Performance in der Entwurfsphase liegt, bevor überhaupt eine Taste auf der Tastatur gedrückt wurde. Wenn das Design oder die Algorithmen eines Programms ineffizient sind, dann wird keine noch so kleine Detailverbesserung dafür sorgen, dass es schnell läuft. Es kann zwar schneller laufen, aber niemals so schnell wie ein Programm, das auf Performance ausgelegt ist.

In der Spiele- und Grafikprogrammierung ist das weitaus wichtiger als in der allgemeinen Programmierung. Ein performantes Design läuft auch ohne Low-Level-Optimierung oft um ein Vielfaches schneller als ein mittelmäßiges Design mit Low-Level-Optimierung.

Inkrementelles Design

In der Praxis ist es natürlich unwahrscheinlich, dass man ohne Vorkenntnisse gleich beim ersten Mal den besten Entwurf findet. Stattdessen werden Sie oft eine Reihe von Versionen eines bestimmten Bereichs des Codes erstellen, die jeweils einen anderen Ansatz für das Problem verfolgen, bis Sie zu einer zufriedenstellenden Lösung kommen. Es ist wichtig, dass Sie in dieser Phase nicht zu viel Zeit auf die Details verwenden, bis Sie den Gesamtentwurf fertiggestellt haben. Andernfalls wird ein Großteil Ihrer Arbeit verworfen werden.

Es ist schwierig, allgemeine Richtlinien für ein performantes Design zu geben, da dies stark von der Problemstellung abhängt. Ein erwähnenswerter Punkt auf der CPU-Seite ist jedoch, dass moderne CPUs fast immer durch die Speicherbandbreite begrenzt sind. Dies hat zu einem Wiederaufleben des datenorientierten Designs geführt, bei dem Datenstrukturen und Algorithmen für die Cache-Lokalität von Daten und den linearen Zugriff entworfen werden, anstatt im Speicher herumzuspringen.

Der Optimierungprozess

Unter der Annahme, dass wir ein vernünftiges Design haben und unsere Lehren aus Knuth ziehen, sollte unser erster Schritt bei der Optimierung darin bestehen, die größten Bottlenecks zu identifizieren - die langsamsten Funktionen, die niedrig hängenden Früchte.

Sobald wir die Geschwindigkeit des langsamsten Bereichs erfolgreich verbessert haben, ist dies möglicherweise nicht mehr das Bottleneck. Wir sollten also erneut testen/profilen und das nächsten Bottleneck finden, auf das wir uns konzentrieren können.

Der Prozess ist also:

  1. Profilen / Bottleneck identifizieren.

  2. Bottleneck optimieren.

  3. Zu Schritt 1 zurückkehren.

Bottlenecks optimieren

Einige Profiler sagen Ihnen sogar, welcher Teil einer Funktion (welche Datenzugriffe, Berechnungen) die Dinge verlangsamt.

Wie beim Entwurf sollten Sie sich zunächst darauf konzentrieren, dass die Algorithmen und Datenstrukturen so gut wie möglich sind. Der Datenzugriff sollte lokal erfolgen (um den CPU-Cache optimal zu nutzen), und oft kann es besser sein, die Daten kompakt zu speichern (nochmal: immer profilen, um die Ergebnisse zu testen). Häufig berechnen Sie aufwendige Berechnungen im Voraus. Dies kann geschehen, indem man die Berechnung beim Laden eines Levels durchführt, eine Datei mit vorberechneten Daten lädt oder einfach die Ergebnisse komplexer Berechnungen in einer Skriptkonstante speichert und deren Wert ausliest.

Sobald die Algorithmen und Daten gut sind, können Sie häufig kleine Änderungen an Routinen vornehmen, um die Performance zu verbessern. Sie können beispielsweise einige Berechnungen aus Schleifen heraus schieben oder verschachtelte for-Schleifen in nicht verschachtelte Schleifen umwandeln. (Dies sollte möglich sein, wenn Sie die Breite oder Höhe eines 2D-Arrays im Voraus kennen.)

Testen Sie das Timing bzw. mögliche Bottlenecks erneut nach jeder Änderung. Einige Änderungen erhöhen die Geschwindigkeit, andere können sich negativ auswirken. Manchmal wird ein kleiner positiver Effekt durch die negativen Aspekte komplexeren Codes aufgewogen, und Sie können diese Optimierung weglassen.

Anhang

Bottleneck-Mathematik

Das Sprichwort "Eine Kette ist nur so stark wie ihr schwächstes Glied" gilt direkt für die Performanceoptimierung. Wenn Ihr Projekt 90% der Zeit in Funktion A verbringt, kann die Optimierung von A einen massiven Einfluss auf die Performance haben.

A: 9 ms
Everything else: 1 ms
Total frame time: 10 ms
A: 1 ms
Everything else: 1ms
Total frame time: 2 ms

In diesem Beispiel wird das Bottleneck A um den Faktor 9x verbessert, was die gesamte Frame-Laufzeit um das 5-fache verringert und somit die Frames pro Sekunde um das 5-fache erhöht.

Wenn jedoch etwas anderes langsam läuft und Ihr Projekt ausbremst, kann dieselbe Verbesserung zu weniger dramatischen Erfolgen führen:

A: 9 ms
Everything else: 50 ms
Total frame time: 59 ms
A: 1 ms
Everything else: 50 ms
Total frame time: 51 ms

In diesem Beispiel ist der tatsächliche Gewinn in Bezug auf die Framerate recht gering, obwohl wir die Funktion A enorm optimiert haben.

In Spielen werden die Dinge noch komplizierter, weil CPU und GPU unabhängig voneinander laufen. Ihre gesamte Frame-Laufzeit wird durch die langsamere der beiden bestimmt.

CPU: 9 ms
GPU: 50 ms
Total frame time: 50 ms
CPU: 1 ms
GPU: 50 ms
Total frame time: 50 ms

In diesem Beispiel haben wir die CPU erneut enorm optimiert, aber die Frame-Laufzeit hat sich nicht verbessert, da wir ein GPU-Bottleneck haben.