Code-Optimierung

Definition und Eigenschaften

Codeoptimierung ist jede Methode zur Codeänderung, um die Codequalität und -effizienz zu verbessern. Ein Programm kann so optimiert werden, dass es kleiner wird, weniger Speicher verbraucht, schneller ausgeführt wird oder weniger Eingabe- / Ausgabeoperationen ausführt.

Die grundlegenden Anforderungen, die Optimierungsmethoden erfüllen sollten, sind, dass ein optimiertes Programm die gleichen Ausgaben und Nebenwirkungen haben muss wie seine nicht optimierte Version. Diese Anforderung kann jedoch ignoriert werden, wenn der Nutzen der Optimierung als wichtiger eingeschätzt wird als die wahrscheinlichen Folgen einer Änderung des Programmverhaltens.

Arten und Stufen der Optimierung

Die Optimierung kann von automatischen Optimierern oder Programmierern durchgeführt werden. Ein Optimierer ist entweder ein spezialisiertes Softwaretool oder eine integrierte Einheit eines Compilers (der sogenannte optimierende Compiler). Moderne Prozessoren können auch die Ausführungsreihenfolge von Codeanweisungen optimieren.

Optimierungen werden in High-Level- und Low-Level-Optimierungen klassifiziert. Optimierungen auf hoher Ebene werden normalerweise vom Programmierer durchgeführt, der abstrakte Entitäten (Funktionen, Prozeduren, Klassen usw.) behandelt.) und berücksichtigt den allgemeinen Rahmen der Aufgabe, das Design eines Systems zu optimieren. Optimierungen auf der Ebene elementarer Strukturblöcke des Quellcodes – Schleifen, Verzweigungen usw. – werden in der Regel auch als High-Level-Optimierungen bezeichnet, während einige Autoren sie in eine separate (“mittlere”) Ebene einteilen (N. Wirth?). Low-Level-Optimierungen werden in der Phase durchgeführt, in der der Quellcode in einen Satz von Maschinenbefehlen kompiliert wird, und in dieser Phase wird normalerweise eine automatisierte Optimierung verwendet. Assembler-Programmierer glauben jedoch, dass keine Maschine, wie perfekt sie auch sein mag, dies besser kann als ein erfahrener Programmierer (dennoch sind sich alle einig, dass ein armer Programmierer viel schlechter abschneiden wird als ein Computer).

Was zu optimieren ist

Bei der manuellen Codeoptimierung steht man vor einem weiteren Problem: Man muss nicht nur wissen, wie genau optimiert werden soll, sondern auch, welcher Teil des Programms optimiert werden soll. Aus verschiedenen Gründen (langsame Eingabevorgänge, der Unterschied in der Arbeitsgeschwindigkeit eines menschlichen Bedieners und eines Computers usw.) werden 90% der Ausführungszeit eines Programms für die Ausführung von nur 10% des Codes aufgewendet (diese Aussage ist eher spekulativ, mit dem Pareto-Prinzip als ein ziemlich zweifelhafter Grund, aber A. Tanenbaum lässt es überzeugend klingen). Da die Optimierung neben der Zeit, die Sie für die Entwicklung des Programms aufgewendet haben, zusätzliche Zeit in Anspruch nimmt, sollten Sie sich besser auf die Optimierung dieser zeitkritischen 10% des Codes konzentrieren, anstatt zu versuchen, das gesamte Programm zu optimieren. Diese Codefragmente werden als Engpässe bezeichnet und können von speziellen Dienstprogrammen – Profilern – erkannt werden, die die Zeit messen können, die verschiedene Teile des Programms zur Ausführung benötigen.

In der Praxis wird die Optimierung jedoch normalerweise nach der Phase der “chaotischen” Programmierung durchgeführt (einschließlich Methoden wie “Kopieren-Einfügen”, “wir werden später sehen”, “So ist es in Ordnung”) und ist daher eine Mischung aus Optimierung als solcher, Refactoring und Bugfixes: Vereinfachung von “seltsamen” Konstrukten wie strlen(path.c_str()), logische Bedingungen wie (a.x != 0 && a.x != 0) und so weiter. Profiler sind bei dieser Art der Optimierung wenig hilfreich. Dennoch können Sie diese Probleme mit statischen Analysetools erkennen, d. H. Tools, die für die Suche nach semantischen Fehlern entwickelt wurden und sich auf eine gründliche Analyse des Quellcodes verlassen. Wie Sie dem oben genannten Beispiel mit der seltsamen Bedingung entnehmen können, kann ineffizienter Code aufgrund von Fehlern auftreten (wie ein Druckfehler in unserem Beispiel, wobei a.x != 0 && ja != 0 sollte stattdessen sein). Ein leistungsstarker statischer Analysator erkennt solche Codefragmente und macht Sie durch Warnmeldungen darauf aufmerksam.

Gute und schlechte Ergebnisse der Optimierung

In der Programmierung sollte fast alles unter dem Gesichtspunkt der Rationalität behandelt werden – Optimierung ist keine Ausnahme. Es wird angenommen, dass Code, der von einem unerfahrenen Assembler-Programmierer geschrieben wurde, 3-5 mal langsamer ist als vom Compiler (Zubkov) generierter Code. Weithin bekannt ist ein Satz von Knuth über frühe Low-Level-Optimierungen (wie Versuche, Operatoren oder Variablen zu sparen): “Vorzeitige Optimierung ist die Wurzel allen Übels”.

Die meisten Programmierer beschweren sich nicht über Optimierungen, die vom Optimierer durchgeführt werden, von denen einige konventionell und obligatorisch sind. Wie zum Beispiel die Tail-Call-Optimierung in funktionalen Sprachen (Tail-Call ist ein Sonderfall der Rekursion, der als Schleife dargestellt werden kann).

Man sollte jedoch verstehen, dass mehrere komplexe Optimierungen auf der Ebene des Maschinencodes eine große Verlangsamung der Kompilierung verursachen können. Der Nutzen, den Sie Ihnen ermöglichen, kann im Vergleich zu allgemeinen Systemdesignoptimierungen (Wirth) viel zu unbedeutend sein. Man sollte auch bedenken, dass moderne Sprachen mit all ihren syntaktischen und semantischen “Schnickschnack” viele Nuancen und Feinheiten haben, so dass ein Programmierer, der mit ihnen nicht vertraut ist, von einem Ergebnis der Optimierung überrascht sein kann.

Nehmen wir zum Beispiel C ++ und die sogenannte Rückgabewertoptimierung, wenn der Compiler das Kopieren eines temporären Objekts vermeidet, das von einer Funktion zurückgegeben wird. Da der Compiler das Kopieren auslässt, wird diese Methode auch als “Kopierelision” bezeichnet. Also, der folgende Code:

#include <iostream> struct C { C() {} C(const C&) { std::cout << "A copy was made.\n"; }}; C f() { return C();} int main() { std::cout << "Hello World!\n"; C obj = f();}

kann mehrere Ausgänge haben:

Hello World!A copy was made.A copy was made.Hello World!A copy was made.Hello World!

So seltsam es auch scheinen mag, alle drei Versionen sind gültig, da der Sprachstandard in solchen Fällen das Weglassen von Aufrufen eines Kopierkonstruktors erlaubt, auch wenn der Konstruktor Nebenwirkungen hat (§12.8 Kopieren von Klassenobjekten, Absatz 15).

Fazit

Daher sollten wir immer in Betracht ziehen, den Programmcode nach Möglichkeit mit speziellen Dienstprogrammen zu optimieren.

PVS-Studio

Im statischen Analysator PVS-Studio ist eine Reihe von Diagnosen implementiert, mit denen Sie Situationen finden können, in denen Code optimiert werden kann. PVS-Studio kann jedoch wie jeder andere statische Analysator nicht als Ersatz für die Profilierungstools dienen. Nur dynamische Programmanalysatoren sind in der Lage, die Engpässe zu identifizieren. Statische Analysatoren wissen nicht, welche Eingabedaten Programme erhalten und wie oft ein bestimmtes Stück Code ausgeführt wird. Deshalb sagen wir, dass der Analysator vorschlägt, einige “Mikrooptimierungen” des Codes zu implementieren, die die Leistungssteigerungen nicht garantieren.

Trotz des betrachteten Nachteils ist PVS-Studio Analyzer eine gute Ergänzung zu Profiling-Tools. Darüber hinaus wird der Code beim Umgang mit PVS-Studio-Warnungen im Zusammenhang mit der Optimierung oft einfacher und kürzer. Dieser Effekt wird im Artikel “Exploring Microoptimizations Using Tizen Code as an Example” näher betrachtet.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.