Betrachten Sie den folgenden Code ( p
ist vom Typ unsigned char*
und bitmap->width
hat einen ganzzahligen Typ, der genau unbekannt ist und davon abhängt, welche Version einer externen Bibliothek wir verwenden):
for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
Lohnt es sich, es zu optimieren [..]
Könnte es einen Fall geben, in dem dies durch Schreiben zu effizienteren Ergebnissen führen könnte:
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0; x < width; ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
... oder ist das für den Compiler trivial zu optimieren?
Was würden Sie als "besseren" Code betrachten?
Anmerkung des Herausgebers (Ike): Für diejenigen, die sich über den Streiktext wundern, war die ursprüngliche Frage, wie formuliert, gefährlich nahe am Gebiet außerhalb des Themas und trotz positiver Rückmeldungen sehr nahe daran, geschlossen zu werden. Diese wurden gestrichen. Bitte bestrafen Sie jedoch nicht die Antwortenden, die diese angeschlagenen Abschnitte der Frage angesprochen haben.
quelle
*p
es vom selben Typ ist wiewidth
dann, ist es nicht trivial zu optimieren, da es innerhalb der Schleife daraufp
verweisenwidth
und es ändern könnte.p
auf den gleichen Speicher wie verweistbitmap->width
. Daher kann ich das erste Beispiel nicht legal auf das zweite optimieren.Antworten:
Auf den ersten Blick dachte ich, der Compiler könnte für beide Versionen eine äquivalente Assembly mit aktivierten Optimierungsflags generieren. Als ich es überprüfte, war ich überrascht, das Ergebnis zu sehen:
Quelle
unoptimized.cpp
Hinweis: Dieser Code soll nicht ausgeführt werden.
Quelle
optimized.cpp
Hinweis: Dieser Code soll nicht ausgeführt werden.
Zusammenstellung
$ g++ -s -O3 unoptimized.cpp
$ g++ -s -O3 optimized.cpp
Montage (nicht optimiert)
Montage (optimiert.s)
diff
Die generierte Assembly für die optimierte Version lädt tatsächlich (
lea
) diewidth
Konstante im Gegensatz zur nicht optimierten Version, die denwidth
Offset bei jeder Iteration berechnet (movq
).Wenn ich Zeit habe, poste ich irgendwann einen Benchmark dafür. Gute Frage.
quelle
const unsigned
anstatt nurunsigned
im nicht optimierten Fall umwandeln.main
zu Test für eine Optimierung. Gcc markiert es absichtlich als kalt und deaktiviert daher einige Optimierungen dafür. Ich weiß nicht, ob das hier der Fall ist, aber das ist eine wichtige Angewohnheit.bitmap
handelt sich um eine globale. Die Nicht-CSEd-Version verwendet einen Speicheroperandencmp
, was in diesem Fall für perf kein Problem darstellt. Wenn es sich um ein lokales Element handelt, kann der Compiler davon ausgehen, dass andere Zeiger nichts davon "wissen" und darauf verweisen können. Es ist keine schlechte Idee, Ausdrücke mit Globalen in temporären Variablen zu speichern, solange dies die Lesbarkeit verbessert (oder nicht beeinträchtigt) oder wenn die Leistung kritisch ist. Wenn nicht viel los ist, können solche Einheimischen normalerweise nur in Registern leben und niemals verschüttet werden.Es gibt tatsächlich nicht genügend Informationen aus Ihrem Code-Snippet, um sie erkennen zu können, und das einzige, woran ich denken kann, ist Aliasing. Aus unserer Sicht ist es ziemlich klar , dass Sie nicht möchten ,
p
undbitmap
um auf die gleiche Stelle im Speicher, aber der Compiler nicht weiß , dass und (wegenp
der Art istchar*
) hat der Compiler diesen Code arbeiten zu lassen , auch wennp
undbitmap
überlappen.Dies bedeutet in diesem Fall, dass, wenn sich die Schleife
bitmap->width
durch den Zeiger ändertp
, dies beimbitmap->width
späteren erneuten Lesen gesehen werden muss , was wiederum bedeutet, dass das Speichern in einer lokalen Variablen unzulässig wäre.Abgesehen davon glaube ich, dass einige Compiler tatsächlich manchmal zwei Versionen desselben Codes generieren (ich habe Indizien dafür gesehen, aber nie direkt nach Informationen darüber gesucht, was der Compiler in diesem Fall tut) und schnell prüfen, ob die Zeiger vorhanden sind Alias und führen Sie den schnelleren Code aus, wenn er feststellt, dass dies in Ordnung ist.
Abgesehen davon stehe ich zu meinem Kommentar, einfach die Leistung der beiden Versionen zu messen. Mein Geld besteht darin, keinen konsistenten Leistungsunterschied zwischen den beiden Versionen des Codes zu sehen.
Meiner Meinung nach sind Fragen wie diese in Ordnung, wenn Sie sich mit Theorien und Techniken zur Compileroptimierung vertraut machen möchten. Sie sind jedoch Zeitverschwendung (eine nutzlose Mikrooptimierung), wenn Ihr Endziel darin besteht, das Programm schneller laufen zu lassen.
quelle
restrict
Qualifizierer in diesem Fall nicht die Antwort auf das Aliasing-Problem?restrict
ist weitgehend ein Hit-and-Miss. MSVC ist der einzige Compiler, den ich gesehen habe, der es richtig zu machen scheint. ICC verliert Aliasing-Informationen durch Funktionsaufrufe, auch wenn sie inline sind. Und GCC erhält normalerweise keinen Vorteil, es sei denn, Sie deklarieren jeden einzelnen Eingabeparameter alsrestrict
(einschließlich)this
für Mitgliedsfunktionen).char
alle Typen Aliase sind. Wenn Sie also ein Zeichen * haben, müssen Sie es verwendenrestrict
alles verwenden. Oder wenn Sie die strengen Aliasing-Regeln von GCC mit deaktiviert haben, wird-fno-strict-aliasing
alles als möglicher Alias angesehen.restrict
ähnliche Semantik in C ++ ist N4150 .Ok, Leute, also habe ich gemessen
GCC -O3
(mit GCC 4.9 unter Linux x64).Es stellt sich heraus, dass die zweite Version 54% schneller läuft!
Also, ich denke Aliasing ist die Sache, ich hatte nicht darüber nachgedacht.
[Bearbeiten]
Ich habe die erste Version mit allen mit definierten Zeigern erneut versucht
__restrict__
, und die Ergebnisse sind dieselben. Seltsam .. Entweder ist Aliasing nicht das Problem, oder aus irgendeinem Grund optimiert der Compiler es selbst mit nicht gut__restrict__
.[Bearbeiten 2]
Ok, ich glaube, ich konnte so ziemlich beweisen, dass Aliasing das Problem ist. Ich wiederholte meinen ursprünglichen Test, diesmal mit einem Array anstelle eines Zeigers:
Und gemessen (musste "-mcmodel = large" verwenden, um es zu verknüpfen). Dann habe ich versucht:
Die Messergebnisse waren die gleichen - Scheint, als ob der Compiler es selbst optimieren konnte.
Dann habe ich die Originalcodes (mit einem Zeiger
p
) ausprobiert , diesmal wenn siep
vom Typ sindstd::uint16_t*
. Auch hier waren die Ergebnisse dieselben - aufgrund des strengen Aliasing. Dann habe ich versucht, mit "-fno-strict-aliasing" zu bauen, und wieder einen Zeitunterschied festgestellt.quelle
Andere Antworten haben darauf hingewiesen, dass das Heben der Zeigeroperation aus der Schleife das definierte Verhalten aufgrund von Aliasing-Regeln ändern kann, die es char ermöglichen, irgendetwas zu aliasen, und daher keine zulässige Optimierung für einen Compiler darstellt, obwohl dies in den meisten Fällen für einen Menschen offensichtlich korrekt ist Programmierer.
Sie haben auch darauf hingewiesen, dass das Heben des Betriebs aus der Schleife normalerweise, aber nicht immer eine Verbesserung unter dem Gesichtspunkt der Leistung darstellt und unter dem Gesichtspunkt der Lesbarkeit häufig negativ ist.
Ich möchte darauf hinweisen, dass es oft einen "dritten Weg" gibt. Anstatt bis zur gewünschten Anzahl von Iterationen zu zählen, können Sie bis auf Null herunterzählen. Dies bedeutet, dass die Anzahl der Iterationen zu Beginn der Schleife nur einmal benötigt wird und danach nicht mehr gespeichert werden muss. Besser noch auf Assembler-Ebene ist häufig kein expliziter Vergleich erforderlich, da bei der Dekrementierungsoperation normalerweise Flags gesetzt werden, die angeben, ob der Zähler sowohl vor (Übertragsflag) als auch nach (Nullflag) der Dekrementierung Null war.
Beachten Sie, dass diese Version der Schleife x-Werte im Bereich 1..width anstelle des Bereichs 0 .. (width-1) liefert. Das spielt in Ihrem Fall keine Rolle, da Sie x eigentlich für nichts verwenden, aber es ist etwas, das Sie beachten sollten. Wenn Sie eine Countdown-Schleife mit x-Werten im Bereich 0 .. (Breite-1) möchten, können Sie dies tun.
Sie können die Casts in den obigen Beispielen auch entfernen, wenn Sie möchten, ohne sich über die Auswirkungen auf die Vergleichsregeln Gedanken machen zu müssen, da Sie mit bitmap-> width nur eine Variable direkt zuweisen.
quelle
x --> 0
, dass der Operator "Downto" angezeigt wird. Ziemlich witzig. PS Ich halte eine Variable für die Endbedingung nicht für negativ für die Lesbarkeit, es kann tatsächlich das Gegenteil sein.static_cast<unsigned>(bitmap->width)
und Verwendenwidth
stattdessen in der Schleife tatsächlich eine Verbesserung der Lesbarkeit darstellt, da der Leser jetzt weniger Dinge pro Zeile analysieren muss. Die Ansichten anderer können jedoch abweichen.do { } while()
, da Sie in ASM Schleifen mit einem bedingten Zweig am Ende erstellen. Die üblichenfor(){}
undwhile(){}
Schleifen erfordern zusätzliche Anweisungen, um die Schleifenbedingung einmal vor der Schleife zu testen, wenn der Compiler nicht beweisen kann, dass sie immer mindestens einmal ausgeführt wird. Verwenden Sie auf jeden Fallfor()
oderwhile()
wenn es nützlich ist, um zu überprüfen, ob die Schleife überhaupt einmal ausgeführt werden soll oder wann sie besser lesbar ist.Das einzige, was hier die Optimierung verhindern kann, ist die strikte Aliasing-Regel . Kurzum :
Die Ausnahme gilt auch für
unsigned
undsigned
char
Zeiger.Dies ist in Ihrem Code der Fall: Sie ändern,
*p
durchp
welche einunsigned char*
, daher muss der Compiler davon ausgehen, dass er darauf verweisen könntebitmap->width
. Daher ist das Caching vonbitmap->width
eine ungültige Optimierung. Dieses optimierungsverhindernde Verhalten wird in der Antwort von YSC gezeigt .Wenn und nur wenn
p
auf einen Nicht-char
und Nicht-decltype(bitmap->width)
Typ verwiesen wird , wäre das Caching eine mögliche Optimierung.quelle
Die ursprünglich gestellte Frage:
Und meine Antwort darauf (eine gute Mischung aus Up- und Down-Stimmen).
Trotz der Abstimmungen (und jetzt des Aliasing-Problems) bin ich damit immer noch als gültige Antwort zufrieden. Wenn Sie nicht wissen, ob es sich lohnt, etwas zu optimieren, ist dies wahrscheinlich nicht der Fall.
Eine ganz andere Frage wäre natürlich:
Muss Ihre Anwendung oder Bibliothek schneller ausgeführt werden als derzeit? Wartet der Benutzer zu lange? Prognostiziert Ihre Software das Wetter von gestern statt von morgen?
Nur Sie können dies wirklich sagen, basierend darauf, wofür Ihre Software gedacht ist und was Ihre Benutzer erwarten.
Vorausgesetzt, Ihre Software muss optimiert werden, müssen Sie als Nächstes mit der Messung beginnen. Profiler sagen Ihnen, wo Ihr Code seine Zeit verbringt. Wenn Ihr Fragment nicht als Engpass angezeigt wird, lassen Sie es am besten in Ruhe. Profiler und andere Messinstrumente zeigen Ihnen auch an, ob Ihre Änderungen einen Unterschied gemacht haben. Es ist möglich, Stunden damit zu verbringen, Code zu optimieren, nur um festzustellen, dass Sie keinen erkennbaren Unterschied gemacht haben.
Wenn Sie keinen "optimierten" Code schreiben, sollte Ihr Code so klar, sauber und präzise wie möglich sein. Das Argument "Vorzeitige Optimierung ist böse" ist keine Entschuldigung für schlampigen oder ineffizienten Code.
Optimierter Code opfert normalerweise einige der oben genannten Attribute für die Leistung. Dies könnte die Einführung zusätzlicher lokaler Variablen, Objekte mit einem größeren als erwarteten Bereich oder sogar die Umkehrung der normalen Schleifenreihenfolge umfassen. All dies ist möglicherweise weniger klar oder prägnant. Dokumentieren Sie daher den Code (kurz!), Warum Sie dies tun.
Bei „langsamem“ Code sind diese Mikrooptimierungen jedoch häufig der letzte Ausweg. Der erste Blick auf Algorithmen und Datenstrukturen. Gibt es eine Möglichkeit, die Arbeit überhaupt zu vermeiden? Können lineare Suchen durch binäre ersetzt werden? Wäre eine verknüpfte Liste hier schneller als ein Vektor? Oder eine Hash-Tabelle? Kann ich Ergebnisse zwischenspeichern? Gute „effiziente“ Entscheidungen zu treffen, kann die Leistung oft um eine Größenordnung oder mehr beeinträchtigen!
quelle
In dieser Situation verwende ich das folgende Muster. Es ist fast so kurz wie der erste Fall von Ihnen und besser als der zweite Fall, da die temporäre Variable lokal in der Schleife bleibt.
Dies ist schneller mit einem weniger als intelligenten Compiler, Debug-Build oder bestimmten Kompilierungsflags.
Edit1 : Es ist gut, eine konstante Operation außerhalb einer Schleife zu platzieren Programmiermuster. Es zeigt das Verständnis der Grundlagen des Maschinenbetriebs, insbesondere in C / C ++. Ich würde argumentieren, dass die Bemühungen, sich zu beweisen, auf Menschen gerichtet sein sollten, die dieser Praxis nicht folgen. Wenn der Compiler für ein gutes Muster bestraft, ist dies ein Fehler im Compiler.
Edit2 :: Ich habe meinen Vorschlag anhand des Originalcodes in vs2013 gemessen und eine Verbesserung von% 1 erhalten. Können wir es besser machen? Eine einfache manuelle Optimierung bietet eine dreifache Verbesserung gegenüber der ursprünglichen Schleife auf einem x64-Computer, ohne auf exotische Anweisungen zurückzugreifen. Der folgende Code setzt ein kleines Endian-System und eine richtig ausgerichtete Bitmap voraus. TEST 0 ist original (9 Sek.), TEST 1 ist schneller (3 Sek.). Ich wette, jemand könnte dies noch schneller machen, und das Ergebnis des Tests würde von der Größe der Bitmap abhängen. Auf jeden Fall wird der Compiler bald in der Lage sein, konstant schnellsten Code zu produzieren. Ich befürchte, dass dies die Zukunft sein wird, in der der Compiler auch eine Programmierer-KI sein wird, sodass wir arbeitslos wären. Schreiben Sie vorerst nur Code, der zeigt, dass Sie wissen, dass keine zusätzliche Operation in der Schleife erforderlich ist.
quelle
Es gibt zwei Dinge zu beachten.
A) Wie oft wird die Optimierung ausgeführt?
Wenn die Antwort nicht sehr häufig ist, beispielsweise nur, wenn ein Benutzer auf eine Schaltfläche klickt, stören Sie sich nicht, wenn Ihr Code dadurch unlesbar wird. Wenn die Antwort 1000 Mal pro Sekunde lautet, möchten Sie wahrscheinlich mit der Optimierung fortfahren. Wenn es auch nur ein bisschen komplex ist, geben Sie unbedingt einen Kommentar ein, um zu erklären, was los ist, um dem nächsten Mann zu helfen, der mitkommt.
B) Wird dies die Wartung / Fehlerbehebung des Codes erschweren?
Wenn Sie keinen großen Leistungszuwachs feststellen, ist es keine gute Idee, Ihren Code kryptisch zu machen, um nur ein paar Taktstriche zu sparen. Viele Leute werden Ihnen sagen, dass jeder gute Programmierer in der Lage sein sollte, sich den Code anzusehen und herauszufinden, was los ist. Das ist wahr. Das Problem ist, dass in der Geschäftswelt die zusätzliche Zeit, um das herauszufinden, Geld kostet. Wenn Sie es also schöner machen können, zu lesen, dann tun Sie es. Deine Freunde werden es dir danken.
Das heißt, ich würde persönlich das B-Beispiel verwenden.
quelle
Der Compiler kann viele Dinge optimieren. In Ihrem Beispiel sollten Sie sich für die Lesbarkeit, die Wartbarkeit und das, was Ihrem Codestandard folgt, entscheiden. Weitere Informationen darüber, was (mit GCC) optimiert werden kann, finden Sie in diesem Blogbeitrag .
quelle
Lassen Sie den Compiler in der Regel die Optimierung für Sie durchführen, bis Sie festgelegt haben, dass Sie übernehmen sollen. Die Logik hierfür hat nichts mit Leistung zu tun, sondern mit menschlicher Lesbarkeit. In den allermeisten Fällen ist die Lesbarkeit Ihres Programms wichtiger als seine Leistung. Sie sollten darauf abzielen, Code zu schreiben, der für einen Menschen leichter zu lesen ist, und sich dann nur dann um die Optimierung kümmern, wenn Sie davon überzeugt sind, dass die Leistung wichtiger ist als die Wartbarkeit Ihres Codes.
Sobald Sie feststellen, dass die Leistung wichtig ist, sollten Sie einen Profiler für den Code ausführen, um festzustellen, welche Schleifen ineffizient sind, und diese einzeln optimieren. Es kann zwar Fälle geben, in denen Sie diese Optimierung durchführen möchten (insbesondere, wenn Sie in Richtung C ++ migrieren, wo STL-Container beteiligt sind), aber die Kosten für die Lesbarkeit sind hoch.
Außerdem kann ich mir pathologische Situationen vorstellen, in denen der Code tatsächlich verlangsamt werden könnte. Stellen Sie sich zum Beispiel den Fall vor, in dem der Compiler nicht beweisen konnte, dass dies während
bitmap->width
des Prozesses konstant war. Durch Hinzufügen derwidth
Variablen zwingen Sie den Compiler, eine lokale Variable in diesem Bereich zu verwalten. Wenn diese zusätzliche Variable aus einem bestimmten plattformspezifischen Grund eine Optimierung des Stapelbereichs verhinderte, muss sie möglicherweise die Ausgabe von Bytecodes neu organisieren und etwas weniger Effizientes erzeugen.Unter Windows x64 muss beispielsweise
__chkstk
in der Präambel der Funktion ein spezieller API-Aufruf aufgerufen werden, wenn die Funktion mehr als eine Seite lokaler Variablen verwendet. Mit dieser Funktion können Fenster die Schutzseiten verwalten, mit denen sie den Stapel bei Bedarf erweitern. Wenn Ihre zusätzliche Variable die Stapelverwendung von unter 1 Seite auf über oder über 1 Seite erhöht, muss Ihre Funktion jetzt bei__chkstk
jeder Eingabe aufrufen . Wenn Sie diese Schleife auf einem langsamen Pfad optimieren, können Sie den schnellen Pfad tatsächlich stärker verlangsamen, als Sie auf dem langsamen Pfad gespeichert haben!Sicher, es ist ein bisschen pathologisch, aber der Sinn dieses Beispiels ist, dass Sie den Compiler tatsächlich verlangsamen können. Es zeigt nur, dass Sie Ihre Arbeit profilieren müssen, um festzustellen, wohin die Optimierungen führen. In der Zwischenzeit sollten Sie die Lesbarkeit in keiner Weise für eine Optimierung opfern, die möglicherweise von Bedeutung ist oder nicht.
quelle
Der Vergleich ist falsch, da die beiden Codefragmente
und
sind nicht gleichwertig
Im ersten Fall
width
ist abhängig und nicht const, und man kann nicht davon ausgehen, dass es sich zwischen nachfolgenden Iterationen nicht ändert. Daher kann es nicht optimiert werden, sondern muss bei jeder Schleife überprüft werden .In Ihrem optimierten Fall wird einer lokalen Variablen
bitmap->width
irgendwann während der Programmausführung der Wert von zugewiesen . Der Compiler kann überprüfen, ob sich dies tatsächlich ändert.Haben Sie über Multithreading nachgedacht, oder könnte der Wert extern abhängig sein, sodass sein Wert volatil ist? Wie würde man erwarten, dass der Compiler all diese Dinge herausfindet, wenn Sie es nicht sagen?
Der Compiler kann nur so gut, wie es Ihr Code zulässt.
quelle
Wenn Sie nicht genau wissen, wie der Compiler den Code optimiert, ist es besser, Ihre eigenen Optimierungen vorzunehmen, indem Sie die Lesbarkeit und das Design des Codes beibehalten. Praktisch ist es schwierig, den Assemblycode für jede Funktion zu überprüfen, die wir für neue Compilerversionen schreiben.
quelle
Der Compiler kann nicht optimieren,
bitmap->width
da der Wert vonwidth
zwischen den Iterationen geändert werden kann. Es gibt einige häufigste Gründe:iterator::end()
odercontainer::size()
so ist es schwer vorherzusagen , ob es immer das gleiche Ergebnis zurückgibt.Um (meine persönliche Meinung) für Orte zusammenzufassen, die ein hohes Maß an Optimierung erfordern, müssen Sie dies selbst tun. An anderen Orten lassen Sie es einfach, der Compiler kann es optimieren oder nicht, wenn es keinen großen Unterschied gibt. Die Lesbarkeit des Codes ist das Hauptziel.
quelle