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 Leistung beizubehalten.

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

  • zügiger 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 Leistungsprobleme häufig zusammengefasst. In Wirklichkeit gibt es jedoch verschiedene Arten von Leistungsproblemen zu differenzieren:

  • Ein langsamer Prozess, der bei jedem Frame auftritt und zu einer kontinuierlich niedrigen Bildwiederholrate 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.

Leistung messen

Das wahrscheinlich wichtigste Instrument zur Optimierung ist die Möglichkeit, die Leistung zu messen - um festzustellen, wo Engpässe liegen, und um den Erfolg zu messen beim Versuch, diese zu beschleunigen.

Es gibt mehrere Optionen zur Leistungsmessung, wie:

  • Einen Start/Stop-Timer um den zu untersuchenden Code setzen.

  • Godot Profiler verwenden.

  • Externe Profiling-Werkzeuge von Drittanbietern verwenden.

  • Verwendung von GPU-Profilern/Debuggern wie z.B. NVIDIA Nsight Graphics oder apitrace.

  • Beobachtung der Bildwiederholrate (mit abgeschaltetem V-Sync).

Besonders zu beachten ist, dass die relative Leistung unterschiedlicher Bereiche je nach Hardware variieren kann. Oft ist es eine gute Idee, Timings auf mehr als einem Gerät vorzunehmen, insbesondere auf Mobilgeräten, sollte dafür entwickelt werden.

Limitierungen

CPU-Profiler sind oft das Mittel der Wahl zur Leistungsmessung, dennoch geben sie nicht zwangsläufig Rückschlüsse auf jedes Problem.

  • Engpässe treten häufig auf der GPU auf, aufgrund von Anweisungen der CPU.

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

  • Bei mobilen Zielplattformen kann Profiling unter Umständen nicht möglich sein.

  • Möglicherweise müssen Leistungsprobleme gelöst werden, die auf Hardware auftreten, auf die der Entwickler keinen Zugriff hat.

Aufgrund dieser Einschränkungen muss häufig Detektivarbeit geleistet werden, um herauszufinden, wo Engpässe vorliegen.

Detektivarbeit

Detektivarbeit ist eine entscheidende Fähigkeit für Entwickler (sowohl in Bezug auf die Leistung als auch in Bezug auf die Fehlerbehebung). Dies kann Hypothesentests und binäre Suche umfassen.

Hypothesentest

Angenommen, das Spiel wird durch Sprites verlangsamt. Um diese Hypothese zu testen, kann:

  • die Leistung gemessen werden, indem weitere Sprites hinzugefügt oder einige entfernt werden.

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

  • Das wiederum kann getestet werden, indem alles beibehalten, aber die Sprite-Größe geändert wird.

Profiler

Mit Profilern kann der Programmablauf zeitlich abgebildet werden. Aus den Ergebnissen geht hervor, wieviel Prozent der Zeit während der Programmausführung in verschiedenen Funktionen und anderen Bereichen verbracht wurde und wie oft Funktionen aufgerufen wurden.

Das kann sehr nützlich sein, um Engpässe 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 Leistung führen. Profiling und Zeitmessung sind wertvolle Mittel, um hier zielgerichtet vorzugehen.

Weitere Informationen zur Verwendung des eingebauten Profilers finden sich unter Debugger Panel.

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 Effizienzversuche 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 Chancen 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 debuggen ist als nicht optimierter Code. Es liegt im eigenen Interesse, dies auf Bereiche zu beschränken, die wesentlich davon profitieren.

Nur weil ein bestimmtes Codestück optimiert werden kann, ist es nicht zwangsläufig eine sinnvolle Maßnahme. Zu wissen, wann und wann nicht zu optimieren, ist eine großartige Fertigkeit.

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 grundsätzlich für sich allein richtige Aussage, vom vorzeitigen Optimieren abzusehen, birgt das Risiko, eine besondere Art der Optimierung zu vernachlässigen, die am Anfang sehr wohl relevant ist - und zwar die eines gut durchdachten Programmentwurfs, welche stattfindet, noch bevor eine Zeile Code geschrieben wird. Sind Design oder Algorithmen eines Programms ineffizient, wird es später sehr schwierig werden, noch Leistung herauszuholen. Es kann schneller laufen, doch es wird niemals so schnell laufen, wie ein Programm mit einem von vornherein leistungsorientierten Konzept.

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

Of course, in practice, unless you have prior knowledge, you are unlikely to come up with the best design the first time. Instead, you'll often make a series of versions of a particular area of code, each taking a different approach to the problem, until you come to a satisfactory solution. It's important not to spend too much time on the details at this stage until you have finalized the overall design. Otherwise, much of your work will be thrown out.

It's difficult to give general guidelines for performant design because this is so dependent on the problem. One point worth mentioning though, on the CPU side, is that modern CPUs are nearly always limited by memory bandwidth. This has led to a resurgence in data-oriented design, which involves designing data structures and algorithms for cache locality of data and linear access, rather than jumping around in memory.

Der Optimierungprozess

Unter der Annahme, dass wir ein vernünftiges Design haben und wir auch fit im programmieren sind, sollte unser erster Schritt bei der Optimierung darin bestehen, die größten Engpässe zu identifizieren - die langsamsten Funktionen.

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

Der Prozess ist also:

  1. Profile / Engpass identifizieren.

  2. Engpass optimieren.

  3. Kehren Sie zu Schritt 1 zurück.

Engpässe optimieren

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

As with design, you should concentrate your efforts first on making sure the algorithms and data structures are the best they can be. Data access should be local (to make best use of CPU cache), and it can often be better to use compact storage of data (again, always profile to test results). Often, you precalculate heavy computations ahead of time. This can be done by performing the computation when loading a level, by loading a file containing precalculated data or simply by storing the results of complex calculations into a script constant and reading its value.

Sobald die Algorithmen und Daten gut sind, können Sie häufig kleine Änderungen an Routinen vornehmen um die Leistung zu verbessern. Sie können beispielsweise einige Berechnungen außerhalb von Schleifen verschieben 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 Engpässe immer wieder 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

Engpass Mathematik

Das Sprichwort "Eine Kette ist nur so stark wie ihr schwächstes Glied" gilt direkt für die Leistungsoptimierung. Wenn Ihr Projekt 90% der Zeit in Funktion A verbringt, kann die Optimierung von A einen massiven Einfluss auf die Leistung 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 der Engpass A um den Faktor 9x verbessert, was die Gesamte Frame-Zeit um das 5-fache verringert und somit die Bilder 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 Bildrate recht gering, obwohl wir die Funktionalität A enorm optimiert haben.

In Spielen werden die Dinge noch komplizierter, weil CPU und GPU unabhängig voneinander laufen. Ihre gesamte Frame-Zeit 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 Bildwiederholrate hat sich nicht verbessert, da wir einen GPU-Engpass haben.