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, während wir eine gute Leistung beibehalten.

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

  • schneller arbeiten

  • intelligenter arbeiten

Und vorzugsweise verwenden wir eine Mischung aus beiden.

Rauch und Spiegel

Part of working smarter is recognizing that, in games, we can often get the player to believe they're in a world that is far more complex, interactive, and graphically exciting than it really is. A good programmer is a magician, and should strive to learn the tricks of the trade while trying to invent new ones.

Die Natur der Langsamkeit

Für den externen Beobachter werden Leistungsprobleme häufig zusammengefasst. In Wirklichkeit gibt es jedoch verschiedene Arten von Leistungsproblemen:

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

  • Ein zeitweise aussetzender Prozess, der 'Spitzen' der Langsamkeit verursacht und zu Verzögerungen führt.

  • Ein langsamer Prozess, der außerhalb des normalen Spiels 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 Fähigkeit, die Leistung zu messen - um festzustellen, wo Engpässe liegen, und um den Erfolg unserer Versuche zu messen, diese zu beschleunigen.

Es gibt mehrere Arten Leistung zu messen, wie:

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

  • Godot Profiler verwenden

  • Externer Profiler von Drittanbietern verwenden

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

  • Überprüfen der Bildwiederholrate (mit abgeschaltetem V-Sync)

Beachten Sie, dass die relative Leistung verschiedener Bereiche je nach Hardware variieren kann. Oft ist es eine gute Idee, Timings auf mehr als einem Gerät vorzunehmen, insbesondere auf Mobilgeräten, wenn Sie dafür entwickeln wollen.

Limitierungen

CPU-Profiler sind häufig 'die' Methode zur Messung der Leistung, sie liefern jedoch nicht immer alle Infos.

  • 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).

  • You may not always be able to profile specific devices like a mobile phone due to the initial setup required.

  • Möglicherweise müssen Sie Leistungsprobleme lösen, die auf Hardware auftreten, auf die Sie keinen Zugriff haben.

Aufgrund dieser Einschränkungen müssen Sie häufig Detektivarbeit leisten um herauszufinden, wo Engpässe liegen.

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 Sie glauben Ihr Spiel wird durch Sprites verlangsamt. Sie können diese Hypothese testen indem Sie:

  • Die Leistung messen, wenn Sie weitere Sprites hinzufügen oder einige entfernen.

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

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

Profiler

Mit Profilern können Sie Ihr Programm zeitlich messen, während es ausgeführt wird. Profiler liefern dann Ergebnisse, aus denen hervorgeht, wie viel Prozent der Zeit in verschiedenen Funktionen und Bereichen verbracht wurden und wie oft Funktionen aufgerufen wurden.

Dies kann sehr nützlich sein, um Engpässe zu identifizieren und die Ergebnisse Ihrer Verbesserungen zu messen. Manchmal können Versuche die Leistung zu verbessern nach hinten losgehen und zu einer langsameren Leistung führen. Verwenden Sie immer Profiler und Timing, um Ihre Bemühungen zu steuern.

Weitere Informationen zur Verwendung des Profilers in Godot finden Sie unter Debugger Panel.

Prinzipien

Die 5 Design-Prinzipien der Objektorientierten Programmierung mit kann mit dem Akronym SOLID zusammenfassen. Donald Knuth erklärte diese einmal auf einfache Weise:

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 Nachrichten sind sehr wichtig:

  • Die Zeit für Programmierer und Entwickler ist begrenzt. Anstatt blindlings zu versuchen alle Aspekte eines Programms zu beschleunigen, sollten wir uns auf die Aspekte konzentrieren, die wirklich wichtig sind.

  • Optimierungsversuche führen häufig zu Code, der schwerer zu lesen und zu debuggen ist als nicht optimierter Code. Es liegt in unserem Interesse, dies auf Bereiche zu beschränken, die wirklich davon profitieren.

Nur weil wir ein bestimmtes Stück Code optimieren können, heißt das nicht unbedingt, dass wir es tun sollten. Zu wissen wann und wann nicht zu optimieren ist, ist eine großartige Fähigkeit die es zu entwickeln gilt.

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

Performantes Design

The danger with encouraging people to ignore optimization until necessary, is that it conveniently ignores that the most important time to consider performance is at the design stage, before a key has even hit a keyboard. If the design or algorithms of a program are inefficient, then no amount of polishing the details later will make it run fast. It may run faster, but it will never run as fast as a program designed for performance.

Dies ist in der Spiel- und Grafikprogrammierung 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.