Welche Arten von C-Optimierungspraktiken in x86, deren Verwendung in der Vergangenheit empfohlen wurde, sind nicht mehr effektiv?
Lesezeit: 8 Minuten
Mys_721tx
Aufgrund der Fortschritte bei x86-C-Compilern (insbesondere GCC und Clang) werden viele Codierungspraktiken, von denen angenommen wurde, dass sie die Effizienz verbessern, nicht mehr verwendet, da die Compiler den Code besser optimieren können als Menschen (z. B. Bitverschiebung vs. Multiplikation).
Welche konkreten Praktiken sind das?
Ich bin mit der knappen Entscheidung nicht einverstanden. Es gibt einige Praktiken, die eindeutig vorteilhaft waren, aber jetzt genauso eindeutig kontraproduktiv sind. Diese Empfehlungen basieren normalerweise auf harten Fakten, und diese harten Fakten ändern sich und ändern die Empfehlungen. Und die Veränderung ist auch eine harte Tatsache.
– Cmaster – Wiedereinsetzung von Monica
5. Juni 2014 um 20:32 Uhr
Vielleicht ist “meinungsbasiert” falsch, aber “zu weit gefasst” ist es sicherlich nicht. Ich meine, soll jeder eine Antwort hinterlassen oder soll eine Person jede Antwort posten? Reden wir über x86 oder sollten wir ARM- und PPC-Prozessoren in Betracht ziehen? Welcher Compiler und auf welcher Optimierungsstufe? Eine viel bessere Frage wäre “Optimierung x wurde historisch empfohlen [citation needed]. Gilt es immer noch mit einem modernen Compiler wie gcc in x86-64-Programmen?”
– individuell
5. Juni 2014 um 20:40 Uhr
Ich warte darauf, dass jemand sagt: “Alles. Es gibt absolut keinen Grund, überhaupt zu optimieren. Der Compiler erledigt alles für Sie. Er macht Ihnen sogar Frühstück.”
– Mystisch
5. Juni 2014 um 20:46 Uhr
Alle. Es gibt absolut keinen Grund, überhaupt zu optimieren. Der Compiler erledigt alles für Sie. Es wird Ihnen sogar Frühstück machen.
– Benutzer1804599
5. Juni 2014 um 20:46 Uhr
Duffs Gerät“Als zahlreiche Instanzen von Duffs Gerät vom XFree86-Server in Version 4.0 entfernt wurden, gab es eine Leistungsverbesserung.”
– Elliot Frisch
5. Juni 2014 um 20:50 Uhr
Von den allgemein empfohlenen Optimierungen sind ein paar, die angesichts moderner Compiler im Grunde nie fruchtbar sind, darunter:
Mathematische Transformationen
Moderne Compiler verstehen Mathematik und führen gegebenenfalls Transformationen an mathematischen Ausdrücken durch.
Optimierungen wie die Umwandlung von Multiplikation in Addition oder konstante Multiplikation oder Division in Bitverschiebung werden von modernen Compilern bereits auf niedrigen Optimierungsstufen durchgeführt. Beispiele für diese Optimierungen sind:
x * 2 -> x + x
x * 2 -> x << 1
Beachten Sie, dass einige Spezifisch Fälle können abweichen. Zum Beispiel, x >> 1 ist nicht dasselbe wie x / 2; es ist nicht angebracht, das eine durch das andere zu ersetzen!
Außerdem sind viele dieser vorgeschlagenen Optimierungen nicht wirklich schneller als der Code, den sie ersetzen.
Dumme Codetricks
Ich bin mir nicht einmal sicher, wie ich das nennen soll, aber Tricks wie XOR-Swapping (a ^= b; b ^= a; a ^= b;) sind überhaupt keine Optimierungen. Sie sind nur Partytricks – sie sind langsamer und zerbrechlicher als der offensichtliche Ansatz. Verwenden Sie sie nicht.
Das register Stichwort
Dieses Schlüsselwort wird von vielen modernen Compilern ignoriert, da seine beabsichtigte Bedeutung (Erzwingen der Speicherung einer Variablen in einem Register) angesichts der aktuellen Registerzuweisungsalgorithmen nicht sinnvoll ist.
Codetransformationen
Compiler führen gegebenenfalls automatisch eine Vielzahl von Codetransformationen durch. Einige dieser Transformationen, die häufig für die manuelle Anwendung empfohlen werden, aber selten nützlich sind, wenn sie so angewendet werden, umfassen:
Loop-Abrollen. (Dies ist oft sogar schädlich, wenn es wahllos angewendet wird, da es die Codegröße aufbläht.)
Funktions-Inlining. (Kennzeichnen Sie eine Funktion als staticund es wird normalerweise an geeigneter Stelle eingebunden, wenn die Optimierung aktiviert ist.)
Loop Unrolling und Force Inlining können immer noch enorme Beschleunigungen bringen, wenn sie richtig gemacht werden. Alles andere, was Sie erwähnen, ist heutzutage fast universell wahr.
– Mystisch
5. Juni 2014 um 21:00 Uhr
@Mystcial: Das Abrollen von Schleifen ist selten nützlich, es sei denn, der Inhalt der Schleife ist absolut trivial und in die meisten Fällen kann der Compiler es ordnungsgemäß entrollen. Ich habe jedoch Fälle gefunden, in denen das manuelle Entrollen immer noch alles übertreffen kann, was ein Compiler tun kann. Ich habe einen Wahnsinn strlen Implementierung, die auf nichts als einer bestimmten Form des Abrollens basiert (und den Vorteilen einer asymptotisch 100% korrekten Verzweigungsvorhersage), die alles übertrifft, was Sie ohne >= 64-Bit-Vektorisierung auf den meisten, wenn nicht allen Bögen tun können.
– R.. GitHub HÖR AUF, EIS ZU HELFEN
5. Juni 2014 um 21:18 Uhr
@R .. In den meisten Fällen, auf die ich stoße, ist der Schleifenkörper eine Abhängigkeitskette von mehr als 100 Zyklen von Gleitkommaanweisungen mit hoher Latenz. Der Compiler weigert sich, es auszurollen, weil es zu groß ist. Und der Körper ist zu groß, um in den CPU-Neuordnungspuffer zu passen. Es gibt also eine Menge ILP, die durch manuelles Entrollen und Verschachteln der Iterationen gewonnen werden kann.
– Mystisch
5. Juni 2014 um 21:21 Uhr
@Mystical: Kannst du solche Dateien mit kompilieren -funroll-all-loopsoder verwenden Sie das Äquivalent #pragma oder __attribute__ auf die Funktionen? Offensichtlich wäre es schön, wenn der Compiler das Interleaving übernehmen würde, und ich denke, dass dies möglich ist, solange Sie Aliasing-Probleme vermeiden (z restrict Stichwort).
– R.. GitHub HÖR AUF, EIS ZU HELFEN
5. Juni 2014 um 21:30 Uhr
@R .. Ich habe es nie über kleine Ausschnitte zum Experimentieren hinaus versucht. Das Hauptproblem, das ich gefunden habe, ist, dass der Compiler nicht verschachtelt, wenn der Körper zu groß ist. Der Grund dafür ist, dass der Scheduling-Algorithmus in O(N^2) läuft. Wenn Sie also (meiner Erfahrung nach) mehr als ein paar hundert davon haben, flippt der Compiler aus und fällt auf etwas anderes zurück. Das Interleaving ist für mich trivial, da ich umfassende Kenntnisse der Datenabhängigkeiten habe. Aber für den Compiler ist es nur ein DAG mit Hunderten oder Tausenden von Nodes und Edges – dessen Verarbeitung einen enormen Arbeitsaufwand erfordert.
– Mystisch
5. Juni 2014 um 21:36 Uhr
cmaster – monica wieder einsetzen
Eine solche Praxis besteht darin, Multiplikationen zu vermeiden, indem Arrays von Array-Zeigern anstelle von echten 2D-Arrays verwendet werden.
Alte Praxis:
int width = 1234, height = 5678;
int* buffer = malloc(width*height*sizeof(*buffer));
int** image = malloc(height*sizeof(*image));
for(int i = height; i--; ) image[i] = &buffer[i*width];
//Now do some heavy computations with image[y][x].
Dies war früher schneller, weil Multiplikationen früher sehr teuer waren (in der Größenordnung von 30 CPU-Zyklen), während Speicherzugriffe praktisch kostenlos waren (erst in den 1990er Jahren wurden Caches hinzugefügt, weil der Speicher nicht mit der vollen CPU mithalten konnte). Geschwindigkeit).
Aber die Multiplikationen wurden schnell, einige CPUs konnten sie in einem CPU-Zyklus ausführen, während die Speicherzugriffe überhaupt nicht Schritt hielten. Also, jetzt ist dieser Code wahrscheinlich performanter:
int width = 1234, height = 5678;
int (*image)[width] = malloc(height*sizeof(*image));
//Now do some heavy computations with image[y][x],
//which will invoke pointer arithmetic to calculate the offset as (y*width + x)*sizeof(int).
Derzeit gibt es noch einige CPUs, bei denen der zweite Code nicht schneller ist, aber die große Multiplikationsstrafe ist bei uns nicht mehr vorhanden.
Technisch: Sie würden nicht mit multiplizieren sizeof(int) im zweiten Beispiel; der Compiler übernimmt diese Skalierung für Sie.
– Jonathan Leffler
5. Juni 2014 um 21:03 Uhr
@JonathanLeffler Wenn ich mich nicht sehr irre, wird diese Multiplikation tatsächlich als Teil des Ladebefehls auf X86-CPUs durchgeführt. Wenn es zu einer eigenen Anweisung kompiliert würde, würde es normalerweise zu einer Bitverschiebung kompiliert. In beiden Fällen muss irgendwo der reale, physische Offset in Bytes explizit oder implizit berechnet werden, und das beinhaltet die Multiplikation mit der Größe von int.
– Cmaster – Wiedereinsetzung von Monica
5. Juni 2014 um 21:07 Uhr
Minor: Sollte das Beispiel nicht image[i] = buffer[i*width] –> image[i] = &buffer[i*width]fehlen &?
– chux – Wiedereinsetzung von Monica
5. Juni 2014 um 21:56 Uhr
@chux Ja. Vielen Dank. Fest.
– Cmaster – Wiedereinsetzung von Monica
6. Juni 2014 um 19:29 Uhr
Aufgrund der Vielzahl von Plattformen würden Sie bestenfalls für eine bestimmte Plattform (oder CPU-Architektur/Modell) und Compiler optimieren!! Wenn Ihr Code auf vielen Plattformen läuft, ist das Zeitverschwendung. (Ich spreche von Micro-Opts, es lohnt sich immer, über bessere Algorithmen nachzudenken)
Dies besagt, dass die Optimierung für eine bestimmte Plattform, DSP, sinnvoll ist, wenn die Notwendigkeit dafür besteht. Dann ist der beste erste Helfer IMHO der vernünftige Einsatz restrict Schlüsselwort, wenn der Compiler/Optimierer es gut unterstützt. Vermeiden Sie Algorithmen mit Bedingungen und sprunghaftem Code (breaks, goto, if, while, …). Dies begünstigt das Streaming und vermeidet zu viele schlechte Vorhersagen von Verzweigungen. Ich würde zustimmen, dass diese Hinweise jetzt gesunder Menschenverstand sind.
Generell würde ich sagen: Jede Manipulation, die den Code modifiziert, indem Annahmen darüber getroffen werden, wie der Compiler optimiert, sollte meiner Meinung nach überhaupt vermieden werden.
Wechseln Sie dann lieber zu Assembly (übliche Praxis für einige wirklich wichtige Algorithmen in DSPs, bei denen die Compiler, obwohl sie wirklich großartig sind, immer noch die letzten paar Prozent der CPU/Mem-Zyklen-Leistungssteigerung verpassen …)
Eine Optimierung, die wirklich nicht mehr verwendet werden sollte, ist #define (Erweitert die Antwort von Duskwuff ein wenig).
Der C-Präprozessor ist eine wunderbare Sache, und er kann einige erstaunliche Codetransformationen durchführen und bestimmten wirklich komplexen Code viel einfacher machen – aber verwenden #define nur um zu bewirken, dass eine kleine Operation inliniert wird, ist es nicht normalerweise mehr angemessen. Die meisten modernen Compiler haben eine real inline Schlüsselwort (oder gleichwertig, wie z __inline__), und sie sind intelligent genug, um die meisten zu inlinen static funktioniert sowieso, was bedeutet, dass Code wie folgt:
#define sum(x, y) ((x) + (y))
ist wirklich besser als die äquivalente Funktion geschrieben:
static int sum(int x, int y)
{
return x + y;
}
Sie vermeiden gefährliche Mehrfachauswertungsprobleme und Nebeneffekte, Sie erhalten eine Überprüfung des Compilertyps und am Ende erhalten Sie auch saubereren Code. Wenn es sich lohnt, einzufügen, wird es der Compiler tun.
Speichern Sie den Präprozessor im Allgemeinen für die Umstände, in denen er benötigt wird: Emittieren einer Menge komplexer, Variante Code bzw teilweise schnell codieren. Die Verwendung des Präprozessors zum Inlinen kleiner Funktionen und zum Definieren von Konstanten ist jetzt hauptsächlich ein Antimuster.
13705500cookie-checkWelche Arten von C-Optimierungspraktiken in x86, deren Verwendung in der Vergangenheit empfohlen wurde, sind nicht mehr effektiv?yes
Ich bin mit der knappen Entscheidung nicht einverstanden. Es gibt einige Praktiken, die eindeutig vorteilhaft waren, aber jetzt genauso eindeutig kontraproduktiv sind. Diese Empfehlungen basieren normalerweise auf harten Fakten, und diese harten Fakten ändern sich und ändern die Empfehlungen. Und die Veränderung ist auch eine harte Tatsache.
– Cmaster – Wiedereinsetzung von Monica
5. Juni 2014 um 20:32 Uhr
Vielleicht ist “meinungsbasiert” falsch, aber “zu weit gefasst” ist es sicherlich nicht. Ich meine, soll jeder eine Antwort hinterlassen oder soll eine Person jede Antwort posten? Reden wir über x86 oder sollten wir ARM- und PPC-Prozessoren in Betracht ziehen? Welcher Compiler und auf welcher Optimierungsstufe? Eine viel bessere Frage wäre “Optimierung x wurde historisch empfohlen [citation needed]. Gilt es immer noch mit einem modernen Compiler wie gcc in x86-64-Programmen?”
– individuell
5. Juni 2014 um 20:40 Uhr
Ich warte darauf, dass jemand sagt: “Alles. Es gibt absolut keinen Grund, überhaupt zu optimieren. Der Compiler erledigt alles für Sie. Er macht Ihnen sogar Frühstück.”
– Mystisch
5. Juni 2014 um 20:46 Uhr
Alle. Es gibt absolut keinen Grund, überhaupt zu optimieren. Der Compiler erledigt alles für Sie. Es wird Ihnen sogar Frühstück machen.
– Benutzer1804599
5. Juni 2014 um 20:46 Uhr
Duffs Gerät“Als zahlreiche Instanzen von Duffs Gerät vom XFree86-Server in Version 4.0 entfernt wurden, gab es eine Leistungsverbesserung.”
– Elliot Frisch
5. Juni 2014 um 20:50 Uhr