Angenommen, Sie haben bereits den Algorithmus der besten Wahl. Welche einfachen Lösungen können Sie anbieten, um die letzten Tropfen der Sweet Sweet Frame Rate aus dem C ++ - Code herauszuholen?
Es versteht sich von selbst, dass diese Tipps nur für den kritischen Codeabschnitt gelten, den Sie bereits in Ihrem Profiler hervorgehoben haben. Es sollte sich jedoch um nicht strukturelle Verbesserungen auf niedriger Ebene handeln. Ich habe ein Beispiel ausgesät.
c++
optimization
Tenpn
quelle
quelle
Antworten:
Optimieren Sie Ihr Datenlayout! (Dies gilt für mehr Sprachen als nur C ++)
Sie können ziemlich tief gehen, indem Sie dies speziell auf Ihre Daten, Ihren Prozessor, den guten Umgang mit Multicore usw. abstimmen. Das grundlegende Konzept lautet jedoch:
Wenn Sie Dinge in einer engen Schleife verarbeiten, möchten Sie die Daten für jede Iteration so klein wie möglich und im Speicher so nah wie möglich beieinander halten. Das heißt, das Ideal ist ein Array oder ein Vektor von Objekten (keine Zeiger), die nur die für die Berechnung erforderlichen Daten enthalten.
Auf diese Weise werden, wenn die CPU die Daten für die erste Iteration Ihrer Schleife abruft, die nächsten Dateniterationen in den Cache geladen.
Die CPU ist wirklich schnell und der Compiler ist gut. Mit weniger und schnelleren Anweisungen kann man nicht wirklich viel anfangen. Die Cache-Kohärenz ist der Ort, an dem sie sich befindet (das ist ein zufälliger Artikel, den ich gegoogelt habe - er enthält ein gutes Beispiel für die Ermittlung der Cache-Kohärenz für einen Algorithmus, der Daten nicht einfach linear durchläuft).
quelle
Ein sehr, sehr niedriger Tipp, der sich jedoch als nützlich erweisen kann:
Die meisten Compiler unterstützen explizite bedingte Hinweise. GCC hat eine Funktion namens __builtin_expect, mit der Sie den Compiler über den Wert eines Ergebnisses informieren können. GCC kann diese Daten verwenden, um die Bedingungen so zu optimieren, dass sie im erwarteten Fall so schnell wie möglich ausgeführt werden. Im unerwarteten Fall ist die Ausführung etwas langsamer.
Ich habe eine 10-20% ige Beschleunigung bei richtiger Verwendung gesehen.
quelle
Das Erste, was Sie verstehen müssen, ist die Hardware, auf der Sie ausgeführt werden. Wie geht es mit Verzweigungen um? Was ist mit Caching? Hat es einen SIMD-Befehlssatz? Wie viele Prozessoren kann es verwenden? Muss es die Prozessorzeit mit irgendetwas anderem teilen?
Möglicherweise lösen Sie dasselbe Problem auf sehr unterschiedliche Weise - selbst die Wahl des Algorithmus sollte von der Hardware abhängen. In einigen Fällen kann O (N) langsamer als O (NlogN) ausgeführt werden (abhängig von der Implementierung).
Als groben Überblick über die Optimierung möchte ich zunächst genau untersuchen, welche Probleme und welche Daten Sie lösen möchten. Dann optimieren Sie das. Wenn Sie extreme Leistung wünschen, dann vergessen Sie generische Lösungen - Sie können alles, was nicht zu Ihrem am häufigsten verwendeten Fall passt, in Sonderfällen ausführen.
Dann profilieren. Profil, Profil, Profil. Betrachten Sie die Speichernutzung, die Verzweigungsstrafen, den Funktionsaufruf-Overhead und die Pipeline-Auslastung. Finden Sie heraus, was Ihren Code verlangsamt. Es ist wahrscheinlich Datenzugriff (ich habe einen Artikel mit dem Titel "The Latency Elephant" über den Overhead des Datenzugriffs geschrieben - google es. Ich kann hier keine zwei Links posten, da ich nicht genug "Reputation" habe) Optimieren Sie dann Ihr Datenlayout ( schöne, große, flache, homogene Arrays sind fantastisch ) und den Datenzugriff (Prefetch, wo möglich).
Wenn Sie den Overhead des Speichersubsystems minimiert haben, versuchen Sie herauszufinden, ob Anweisungen jetzt der Engpass sind (hoffentlich), und sehen Sie sich dann die SIMD-Implementierungen Ihres Algorithmus an - SoA-Implementierungen (Structure-of-Arrays) können sehr datenintensiv sein Befehlscache effizient. Wenn SIMD für Ihr Problem nicht geeignet ist, sind möglicherweise Codierungen auf Intrinsics- und Assembler-Ebene erforderlich.
Wenn Sie noch mehr Geschwindigkeit benötigen, gehen Sie parallel. Wenn Sie den Vorteil haben, auf einer PS3 zu laufen, sind die SPUs Ihre Freunde. Benutze sie, liebe sie. Wenn Sie bereits eine SIMD-Lösung geschrieben haben, profitieren Sie massiv von SPU.
Und dann noch ein paar mehr. Test in Spielszenarien - ist dieser Code immer noch der Engpass? Können Sie die Art und Weise ändern, wie dieser Code auf einer höheren Ebene verwendet wird, um seine Verwendung zu minimieren (dies sollte eigentlich Ihr erster Schritt sein)? Können Sie Berechnungen auf mehrere Frames verschieben?
Erfahren Sie auf jeder Plattform so viel wie möglich über die verfügbare Hardware und die verfügbaren Profiler. Gehen Sie nicht davon aus, dass Sie den Engpass kennen - finden Sie ihn mit Ihrem Profiler. Und stellen Sie sicher, dass Sie eine Heuristik haben, um festzustellen, ob Sie Ihr Spiel tatsächlich schneller gemacht haben.
Und dann nochmal profilieren.
quelle
Erster Schritt: Denken Sie sorgfältig über Ihre Daten in Bezug auf Ihre Algorithmen nach. O (log n) ist nicht immer schneller als O (n). Einfaches Beispiel: Eine Hash-Tabelle mit nur wenigen Schlüsseln wird häufig besser durch eine lineare Suche ersetzt.
Zweiter Schritt: Sehen Sie sich die erzeugte Baugruppe an. C ++ bringt eine Menge impliziter Code-Generierung in die Tabelle. Manchmal schleicht es sich an dich heran, ohne dass du es weißt.
Vorausgesetzt aber, es ist wirklich eine Zeit, in der alles auf den Punkt gebracht wird: Profil. Ernsthaft. Das zufällige Anwenden von "Leistungstricks" schadet ebenso wie hilft.
Dann hängt alles von Ihren Engpässen ab.
Datencachefehler => Optimieren Sie Ihr Datenlayout. Hier ist ein guter Ausgangspunkt: http://gamesfromwithin.com/data-oriented-design
Code - Cache fehlt => Schauen Sie sich virtuelle Funktionsaufrufe, übermäßige Aufrufliste Tiefe usw. Eine häufige Ursache für schlechte Leistung ist der Irrglaube , dass Basisklassen müssen virtuell sein.
Andere gängige C ++ - Leistungseinbußen:
Alle oben genannten Punkte sind beim Betrachten der Baugruppe sofort ersichtlich, siehe oben;)
quelle
Entfernen Sie unnötige Zweige
Auf einigen Plattformen und bei einigen Compilern können Verzweigungen Ihre gesamte Pipeline verwerfen, sodass selbst unbedeutende if () -Blöcke teuer sein können.
Die PowerPC-Architektur (PS3 / x360) bietet die Gleitkomma-Auswahlanweisung
fsel
. Dies kann anstelle einer Verzweigung verwendet werden, wenn die Blöcke einfache Zuweisungen sind:Wird:
Wenn der erste Parameter größer oder gleich 0 ist, wird der zweite Parameter zurückgegeben, andernfalls der dritte.
Der Preis für den Verlust der Verzweigung ist, dass sowohl der if {} - als auch der else {} -Block ausgeführt werden. Wenn also eine teure Operation ausgeführt wird oder ein NULL-Zeiger dereferenziert wird, ist diese Optimierung nicht geeignet.
Manchmal hat Ihr Compiler diese Arbeit bereits ausgeführt. Überprüfen Sie daher zuerst Ihre Assembly.
Hier finden Sie weitere Informationen zu branching und fsel:
http://assemblyrequired.crashworks.org/tag/intrinsics/
quelle
Vermeiden Sie unter allen Umständen Speicherzugriffe, insbesondere zufällige.
Das ist das Wichtigste, was bei modernen CPUs optimiert werden muss. Sie können in der Zeit, in der Sie auf Daten aus dem RAM warten, eine Menge Arithmetik und sogar viele falsch vorhergesagte Verzweigungen ausführen.
Sie können diese Regel auch umgekehrt lesen: Führen Sie zwischen den Speicherzugriffen so viele Berechnungen wie möglich durch.
quelle
Verwenden Sie Compiler Intrinsics.
Stellen Sie sicher, dass der Compiler für bestimmte Vorgänge die effizienteste Assembly generiert, indem Sie intrinsics - Konstrukte verwenden, die aussehen, als würden Funktionsaufrufe vom Compiler in eine optimierte Assembly umgewandelt:
Hier finden Sie eine Referenz für Visual Studio und eine Referenz für GCC
quelle
Entfernen Sie unnötige virtuelle Funktionsaufrufe
Der Versand einer virtuellen Funktion kann sehr langsam sein. Dieser Artikel gibt eine gute Erklärung, warum. Vermeiden Sie Funktionen, die viele, viele Male pro Frame aufgerufen werden, wenn möglich.
Sie können dies auf verschiedene Arten tun. Manchmal können Sie die Klassen einfach umschreiben, damit sie nicht vererbt werden müssen. Vielleicht stellt sich heraus, dass MachineGun die einzige Unterklasse von Weapon ist und Sie können sie zusammenführen.
Sie können Vorlagen verwenden, um den Laufzeit-Polymorphismus durch den Kompilierungs-Polymorphismus zu ersetzen. Dies funktioniert nur, wenn Sie den Untertyp Ihrer Objekte zur Laufzeit kennen und eine umfangreiche Umschreibung durchführen können.
quelle
Mein Grundprinzip ist: Mach nichts, was nicht nötig ist .
Wenn Sie festgestellt haben, dass eine bestimmte Funktion ein Engpass ist, können Sie die Funktion optimieren oder versuchen, den Aufruf zunächst zu unterbinden.
Dies bedeutet nicht unbedingt, dass Sie einen schlechten Algorithmus verwenden. Dies kann bedeuten, dass Sie alle Frames berechnen, die beispielsweise für kurze Zeit zwischengespeichert (oder vollständig vorberechnet) wurden.
Ich probiere diesen Ansatz immer aus, bevor ich mich um eine wirklich einfache Optimierung bemühe.
quelle
Verwenden Sie SIMD (per SSE), falls Sie dies noch nicht getan haben. Gamasutra hat einen schönen Artikel dazu . Sie können den Quellcode aus der vorgestellten Bibliothek am Ende des Artikels herunterladen.
quelle
Minimieren Sie die Abhängigkeitsketten, um die CPU-Pipeline besser nutzen zu können.
In einfachen Fällen kann der Compiler dies für Sie tun, wenn Sie das Abrollen der Schleife aktivieren. Dies ist jedoch häufig nicht der Fall, insbesondere wenn es sich um Floats handelt, da die Neuanordnung der Ausdrücke das Ergebnis ändert.
Beispiel:
quelle
Übersehen Sie Ihren Compiler nicht - wenn Sie gcc unter Intel verwenden, können Sie leicht einen Leistungsgewinn erzielen, indem Sie beispielsweise auf den Intel C / C ++ Compiler umsteigen. Wenn Sie auf eine ARM-Plattform abzielen, lesen Sie den kommerziellen Compiler von ARM. Wenn Sie mit dem iPhone arbeiten, hat Apple lediglich zugelassen, dass Clang ab dem iOS 4.0 SDK verwendet wird.
Ein Problem, auf das Sie wahrscheinlich bei der Optimierung stoßen werden, insbesondere beim x86, ist, dass viele intuitive Dinge bei modernen CPU-Implementierungen gegen Sie arbeiten. Leider ist es für die meisten von uns längst nicht mehr möglich, den Compiler zu optimieren. Der Compiler kann Anweisungen im Stream basierend auf seinem eigenen internen Wissen über die CPU planen. Darüber hinaus kann die CPU Anweisungen auf der Grundlage ihrer eigenen Anforderungen neu planen. Auch wenn Sie sich eine optimale Methode zum Anordnen überlegen, ist die Wahrscheinlichkeit groß, dass der Compiler oder die CPU sich das schon ausgedacht und diese Optimierung bereits durchgeführt hat.
Mein bester Rat wäre, die Optimierungen auf niedriger Ebene zu ignorieren und sich auf die übergeordneten zu konzentrieren. Der Compiler und die CPU können Ihren Algorithmus nicht von einem O (n ^ 2) zu einem O (1) -Algorithmus ändern, egal wie gut sie werden. Sie müssen sich genau ansehen, was Sie tun möchten, und einen besseren Weg finden, dies zu tun. Lassen Sie den Compiler und die CPU sich Gedanken über die niedrige Stufe machen, und konzentrieren Sie sich auf die mittleren bis hohen Stufen.
quelle
Das Schlüsselwort restricted ist möglicherweise nützlich, insbesondere in Fällen, in denen Sie Objekte mit Zeigern bearbeiten müssen. Auf diese Weise kann der Compiler davon ausgehen, dass das Objekt, auf das verwiesen wird, nicht auf andere Weise geändert wird, was wiederum eine aggressivere Optimierung ermöglicht, z. B. Teile des Objekts in Registern zu halten oder Lese- und Schreibvorgänge effektiver neu zu ordnen.
Eine gute Sache an dem Schlüsselwort ist, dass es ein Hinweis ist, den Sie einmal anwenden können, um die Vorteile zu sehen, ohne Ihren Algorithmus neu zu ordnen. Die schlechte Seite ist, dass wenn Sie es an der falschen Stelle verwenden, Sie möglicherweise Datenbeschädigung sehen. Normalerweise ist es jedoch recht einfach zu erkennen, wo es legitim ist, es zu verwenden - eines der wenigen Beispiele, bei denen der Programmierer vernünftigerweise mehr wissen muss, als der Compiler mit Sicherheit annehmen kann, weshalb das Schlüsselwort eingeführt wurde.
Technisch gesehen gibt es in C ++ keine Einschränkung, aber für die meisten C ++ - Compiler sind plattformspezifische Entsprechungen verfügbar.
Siehe auch: http://cellperformance.beyond3d.com/articles/2006/05/demystifying-the-restrict-keyword.html
quelle
Const alles!
Je mehr Informationen Sie dem Compiler über die Daten geben, desto besser sind die Optimierungen (zumindest nach meiner Erfahrung).
wird;
Der Compiler weiß jetzt, dass sich der Zeiger x nicht ändern wird und dass sich auch die Daten, auf die er zeigt, nicht ändern werden.
Der andere zusätzliche Vorteil ist, dass Sie die Anzahl versehentlicher Fehler reduzieren können, indem Sie sich selbst (oder andere) davon abhalten, Dinge zu ändern, die sie nicht sollten.
quelle
const
verbessert keine Compiler-Optimierungen. Richtig, der Compiler kann besseren Code generieren, wenn er weiß, dass sich eine Variable nicht ändert, aberconst
keine ausreichende Garantie bietet.In den meisten Fällen können Sie am besten die Leistung steigern, indem Sie Ihren Algorithmus ändern. Je weniger allgemein die Implementierung ist, desto näher kommt man dem Metall.
Vorausgesetzt, das wurde getan ...
Wenn es sich in der Tat um wirklich kritischen Code handelt, versuchen Sie, Speicherlesevorgänge zu vermeiden, und vermeiden Sie die Berechnung von Daten, die vorberechnet werden können (obwohl keine Nachschlagetabellen vorhanden sind, da sie gegen Regel 1 verstoßen). Wissen Sie, was Ihr Algorithmus macht, und schreiben Sie ihn so, dass der Compiler ihn auch kennt. Überprüfen Sie die Baugruppe, um sicherzustellen, dass dies der Fall ist.
Vermeiden Sie Cache-Fehler. Batch-Prozess so viel wie möglich. Vermeiden Sie virtuelle Funktionen und andere Indirektionen.
Letztendlich alles messen. Die Regeln ändern sich ständig. Was vor 3 Jahren den Code beschleunigte, verlangsamt ihn jetzt. Ein schönes Beispiel ist "Verwenden Sie doppelte mathematische Funktionen anstelle von Float-Versionen". Ich hätte das nicht bemerkt, wenn ich es nicht gelesen hätte.
Ich habe vergessen: Lassen Sie Ihre Variablen nicht von Standardkonstruktoren initialisieren. Wenn Sie darauf bestehen, erstellen Sie zumindest auch Konstruktoren, die dies nicht tun. Beachten Sie die Dinge, die in den Profilen nicht angezeigt werden. Wenn Sie einen unnötigen Zyklus pro Codezeile verlieren, wird in Ihrem Profiler nichts angezeigt, aber insgesamt gehen viele Zyklen verloren. Wieder wissen, was Ihr Code tut. Machen Sie Ihre Kernfunktion schlank statt narrensicher. Narrensichere Versionen können bei Bedarf aufgerufen werden, werden aber nicht immer benötigt. Vielseitigkeit kommt zu einem Preis - Leistung ist eins.
Bearbeitet, um zu erklären, warum es keine Standardinitialisierung gibt: Viel Code sagt: Vector3 bla; bla = DoSomething ();
Die Initialisierung im Konstruktor ist Zeitverschwendung. Auch in diesem Fall ist die verschwendete Zeit gering (wahrscheinlich wird der Vektor gelöscht). Wenn Ihre Programmierer dies jedoch gewohnheitsmäßig tun, summiert sich dies. Außerdem erzeugen viele Funktionen einen temporären Operator (denken Sie an überladene Operatoren), der sofort auf Null initialisiert und zugewiesen wird. Versteckte verlorene Zyklen, die zu klein sind, um einen Spitzenwert in Ihrem Profiler zu sehen, aber die Zyklen über Ihre gesamte Codebasis verteilen. Manche Leute machen auch viel mehr mit Konstruktoren (was offensichtlich ein Nein-Nein ist). Ich habe Multi-Millisekunden-Gewinne von einer nicht verwendeten Variablen gesehen, bei der der Konstruktor zufällig etwas zu schwer war. Sobald der Konstruktor Nebenwirkungen verursacht, kann der Compiler ihn nicht mehr deaktivieren. Wenn Sie also nicht den obigen Code verwenden, bevorzuge ich entweder einen nicht initialisierenden Konstruktor oder, wie gesagt,
Vector3 bla (noInit); bla = doSomething ();
quelle
const Vector3 = doSomething()
? Dann kann die Rückgabewertoptimierung ansetzen und wahrscheinlich die eine oder andere Aufgabe auslösen.Reduzieren Sie die Auswertung von Booleschen Ausdrücken
Dieser ist wirklich verzweifelt, da es sich um eine sehr subtile, aber gefährliche Änderung an Ihrem Code handelt. Wenn Sie jedoch eine Bedingung haben, die übermäßig oft ausgewertet wird, können Sie den Aufwand für die boolesche Auswertung verringern, indem Sie stattdessen bitweise Operatoren verwenden. Damit:
Wird:
Verwenden Sie stattdessen Integer-Arithmetik. Wenn Ihre foos und bars Konstanten sind oder vor if () ausgewertet werden, ist dies möglicherweise schneller als die normale Boolesche Version.
Als Bonus hat die arithmetische Version weniger Verzweigungen als die reguläre Boolesche Version. Welches ist ein anderer Weg, um zu optimieren .
Der große Nachteil ist, dass Sie die faule Bewertung verlieren - der gesamte Block wird bewertet, so dass Sie nicht tun können
foo != NULL & foo->dereference()
. Aus diesem Grund ist es fraglich, ob dies schwierig beizubehalten ist, weshalb der Kompromiss möglicherweise zu groß ist.quelle
Behalten Sie Ihre Stapelverwendung im Auge
Alles, was Sie dem Stapel hinzufügen, ist ein zusätzlicher Push und eine zusätzliche Konstruktion, wenn eine Funktion aufgerufen wird. Wenn viel Stapelspeicher benötigt wird, kann es manchmal nützlich sein, Arbeitsspeicher vorab zuzuweisen, und wenn auf der Plattform, auf der Sie arbeiten, schneller Arbeitsspeicher zur Verfügung steht - umso besser!
quelle