Einer der angegebenen Gründe für die Kenntnis des Assemblers ist, dass er gelegentlich zum Schreiben von Code verwendet werden kann, der leistungsfähiger ist als das Schreiben dieses Codes in einer höheren Sprache, insbesondere C. Ich habe jedoch auch oft gehört, dass, obwohl dies nicht ganz falsch ist, die Fälle, in denen Assembler tatsächlich verwendet werden können, um leistungsfähigeren Code zu generieren, äußerst selten sind und Expertenwissen und Erfahrung mit Assembler erfordern.
Diese Frage bezieht sich nicht einmal auf die Tatsache, dass Assembler-Anweisungen maschinenspezifisch und nicht portierbar sind, oder auf einen der anderen Aspekte von Assembler. Neben dieser gibt es natürlich viele gute Gründe, Assembler zu kennen, aber dies soll eine spezifische Frage sein, die Beispiele und Daten anfordert, und kein erweiterter Diskurs über Assembler im Vergleich zu höheren Sprachen.
Kann jemand einige konkrete Beispiele für Fälle nennen, in denen die Assemblierung mit einem modernen Compiler schneller ist als gut geschriebener C-Code, und können Sie diese Behauptung mit Profiling-Beweisen unterstützen? Ich bin ziemlich zuversichtlich, dass es diese Fälle gibt, aber ich möchte wirklich genau wissen, wie esoterisch diese Fälle sind, da dies ein Streitpunkt zu sein scheint.
quelle
-O3
Flagge hat, sollten Sie die Optimierung wahrscheinlich dem C-CompilerAntworten:
Hier ist ein Beispiel aus der Praxis: Festpunktmultiplikationen auf alten Compilern.
Diese sind nicht nur für Geräte ohne Gleitkomma nützlich, sie glänzen auch in Bezug auf die Genauigkeit, da sie Ihnen eine Genauigkeit von 32 Bit mit einem vorhersagbaren Fehler bieten (float hat nur 23 Bit und es ist schwieriger, einen Genauigkeitsverlust vorherzusagen). dh gleichmäßige absolute Präzision über den gesamten Bereich anstelle einer nahezu gleichmäßigen relativen Präzision (
float
).Moderne Compiler optimieren dieses Festkomma-Beispiel sehr gut. Weitere moderne Beispiele, die noch compilerspezifischen Code benötigen, finden Sie unter
uint64_t
32x32 => 64-Bit-Multiplikationen verwendet, kann auf einer 64-Bit-CPU nicht optimiert werden__int128
effizienten Code auf 64-Bit-Systemen.C hat keinen Vollmultiplikationsoperator (2N-Bit-Ergebnis von N-Bit-Eingängen). Die übliche Art, es in C auszudrücken, besteht darin, die Eingaben in den breiteren Typ umzuwandeln und zu hoffen, dass der Compiler erkennt, dass die oberen Bits der Eingaben nicht interessant sind:
Das Problem mit diesem Code ist, dass wir etwas tun, das nicht direkt in der C-Sprache ausgedrückt werden kann. Wir wollen zwei 32-Bit-Zahlen multiplizieren und ein 64-Bit-Ergebnis erhalten, von dem wir das mittlere 32-Bit zurückgeben. In C existiert diese Multiplikation jedoch nicht. Alles, was Sie tun können, ist, die Ganzzahlen auf 64 Bit zu erhöhen und eine 64 * 64 = 64-Multiplikation durchzuführen.
x86 (und ARM, MIPS und andere) können jedoch die Multiplikation in einem einzigen Befehl durchführen. Einige Compiler haben diese Tatsache ignoriert und Code generiert, der eine Laufzeitbibliotheksfunktion aufruft, um die Multiplikation durchzuführen. Die Verschiebung um 16 erfolgt häufig auch durch eine Bibliotheksroutine (auch der x86 kann solche Verschiebungen durchführen).
Wir haben also nur noch ein oder zwei Bibliotheksaufrufe für eine Multiplikation. Dies hat schwerwiegende Folgen. Die Verschiebung ist nicht nur langsamer, die Register müssen über die Funktionsaufrufe hinweg erhalten bleiben, und es hilft auch nicht beim Inlining und Abrollen des Codes.
Wenn Sie denselben Code im (Inline-) Assembler neu schreiben, können Sie einen deutlichen Geschwindigkeitsschub erzielen.
Darüber hinaus ist die Verwendung von ASM nicht der beste Weg, um das Problem zu lösen. Bei den meisten Compilern können Sie einige Assembler-Anweisungen in intrinsischer Form verwenden, wenn Sie sie nicht in C ausdrücken können. Der VS.NET2008-Compiler macht beispielsweise die 32 * 32 = 64-Bit-Mul als __emul und die 64-Bit-Verschiebung als __ll_rshift verfügbar.
Mithilfe von Intrinsics können Sie die Funktion so umschreiben, dass der C-Compiler die Möglichkeit hat, zu verstehen, was vor sich geht. Dies ermöglicht es, den Code einzubinden, das Register zuzuweisen, die Eliminierung gemeinsamer Unterausdrücke durchzuführen und eine konstante Weitergabe durchzuführen. Auf diese Weise erhalten Sie eine enorme Leistungsverbesserung gegenüber dem handgeschriebenen Assembler-Code.
Als Referenz: Das Endergebnis für das Festkomma-Mul für den VS.NET-Compiler lautet:
Der Leistungsunterschied von Festkomma-Teilungen ist noch größer. Ich hatte Verbesserungen bis zu Faktor 10 für den teilungslastigen Fixpunktcode, indem ich ein paar Asm-Zeilen schrieb.
Die Verwendung von Visual C ++ 2013 bietet für beide Möglichkeiten denselben Assemblycode.
gcc4.1 von 2007 optimiert auch die reine C-Version gut. (Im Godbolt-Compiler-Explorer sind keine früheren Versionen von gcc installiert, aber vermutlich könnten sogar ältere GCC-Versionen dies ohne Eigenheiten tun.)
Siehe source + asm für x86 (32-Bit) und ARM im Godbolt-Compiler-Explorer . (Leider gibt es keine Compiler, die alt genug sind, um schlechten Code aus der einfachen reinen C-Version zu erzeugen.)
Moderne CPUs können Dinge tun , C nicht über Operatoren für überhaupt , wie
popcnt
oder Bit-Scan den ersten oder letzten Satz Bit zu finden . (POSIX hat eineffs()
Funktion, aber die Semantik stimmt nicht mit x86bsf
/ übereinbsr
. Siehe https://en.wikipedia.org/wiki/Find_first_set ).Einige Compiler können manchmal eine Schleife erkennen, die die Anzahl der gesetzten Bits in einer Ganzzahl zählt, und sie zu einem
popcnt
Befehl kompilieren (sofern dies zur Kompilierungszeit aktiviert ist). Die Verwendung__builtin_popcnt
in GNU C oder auf x86 ist jedoch viel zuverlässiger, wenn Sie nur sind Targeting-Hardware mit SSE4.2:_mm_popcnt_u32
von<immintrin.h>
.Oder weisen Sie in C ++ a zu
std::bitset<32>
und verwenden Sie.count()
. (Dies ist ein Fall, in dem die Sprache einen Weg gefunden hat, eine optimierte Implementierung von Popcount über die Standardbibliothek portabel verfügbar zu machen, so dass immer eine korrekte Kompilierung möglich ist und alle vom Ziel unterstützten Vorteile genutzt werden können.) Siehe auch https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .In ähnlicher Weise
ntohl
kann aufbswap
(x86 32-Bit-Byte-Swap für Endian-Konvertierung) auf einigen C-Implementierungen, die es haben , kompiliert werden .Ein weiterer wichtiger Bereich für Intrinsics oder handgeschriebene ASM ist die manuelle Vektorisierung mit SIMD-Anweisungen. Compiler sind nicht schlecht mit einfachen Schleifen wie
dst[i] += src[i] * 10.0;
, aber oft schlecht oder gar nicht automatisch vektorisieren, wenn die Dinge komplizierter werden. Zum Beispiel ist es unwahrscheinlich, dass Sie so etwas wie " Atoi mit SIMD implementieren" erhalten. Wird vom Compiler automatisch aus skalarem Code generiert.quelle
Vor vielen Jahren brachte ich jemandem das Programmieren in C bei. Die Übung bestand darin, eine Grafik um 90 Grad zu drehen. Er kam mit einer Lösung zurück, die einige Minuten in Anspruch nahm, hauptsächlich weil er Multiplikationen und Divisionen usw. verwendete.
Ich zeigte ihm, wie man das Problem mithilfe von Bitverschiebungen neu formuliert, und die Zeit für die Verarbeitung betrug auf dem nicht optimierenden Compiler, den er hatte, ungefähr 30 Sekunden.
Ich hatte gerade einen optimierenden Compiler und der gleiche Code drehte die Grafik in <5 Sekunden. Ich schaute auf den Assembler-Code, den der Compiler generierte, und nach dem, was ich sah, entschied ich, dass meine Tage des Schreibens von Assembler vorbei waren.
quelle
add di,di / adc al,al / add di,di / adc ah,ah
usw. für alle acht 8-Bit-Register lädt , dann alle 8 Register erneut ausführt und dann die gesamte Prozedur drei wiederholt mehrmals und schließlich vier Wörter in ax / bx / cx / dx speichern. Auf keinen Fall wird ein Assembler dem nahe kommen.Fast immer, wenn der Compiler Gleitkomma-Code sieht, ist eine handgeschriebene Version schneller, wenn Sie einen alten fehlerhaften Compiler verwenden. ( Update 2019: Dies gilt im Allgemeinen nicht für moderne Compiler. Insbesondere beim Kompilieren für etwas anderes als x87 haben Compiler im Gegensatz zu x87 eine einfachere Zeit mit SSE2 oder AVX für die Skalarmathematik oder mit Nicht-x86 mit einem flachen FP-Registersatz Registerstapel.)
Der Hauptgrund ist, dass der Compiler keine robusten Optimierungen durchführen kann. Siehe diesen Artikel von MSDN eine Diskussion zu diesem Thema. Hier ist ein Beispiel, in dem die Assembly-Version doppelt so schnell ist wie die C-Version (kompiliert mit VS2K5):
Und einige Zahlen von meinem PC, auf dem ein Standard-Release-Build * ausgeführt wird :
Aus Interesse habe ich die Schleife mit einem dec / jnz getauscht und es machte keinen Unterschied für das Timing - manchmal schneller, manchmal langsamer. Ich denke, der speicherbegrenzte Aspekt stellt andere Optimierungen in den Schatten. (Anmerkung des Herausgebers: Wahrscheinlicher ist, dass der Engpass bei der FP-Latenz ausreicht, um die zusätzlichen Kosten für zu verbergen
loop
. Wenn Sie zwei Kahan-Summierungen parallel für die ungeraden / geraden Elemente durchführen und diese am Ende hinzufügen, kann dies möglicherweise um den Faktor 2 beschleunigt werden. )Hoppla, ich habe eine etwas andere Version des Codes ausgeführt und die Zahlen falsch herum ausgegeben (dh C war schneller!). Die Ergebnisse wurden korrigiert und aktualisiert.
quelle
-ffast-math
. Sie haben eine Optimierungsstufe,-Ofast
die derzeit gleichwertig ist-O3 -ffast-math
, aber in Zukunft möglicherweise weitere Optimierungen enthalten, die in Eckfällen zu einer falschen Codegenerierung führen können (z. B. Code, der auf IEEE-NaNs basiert).a+b == b+a
), aber nicht assoziativ (Neuordnung von Operationen, daher ist die Rundung von Zwischenprodukten unterschiedlich). re: this code: Ich denke nicht, dass unkommentiertes x87 und eineloop
Anweisung eine großartige Demonstration von Fast Asm sind.loop
ist anscheinend kein Engpass aufgrund der FP-Latenz. Ich bin mir nicht sicher, ob er FP-Operationen leitet oder nicht. x87 ist für Menschen schwer zu lesen. Zweifstp results
Insns am Ende sind eindeutig nicht optimal. Das zusätzliche Ergebnis aus dem Stapel zu entfernen, wäre besser mit einem Nicht-Speicher. Wiefstp st(0)
IIRC.Ohne ein bestimmtes Beispiel oder einen Profiler-Beweis anzugeben, können Sie einen besseren Assembler als den Compiler schreiben, wenn Sie mehr als den Compiler wissen.
Im Allgemeinen weiß ein moderner C-Compiler viel mehr darüber, wie der betreffende Code optimiert werden kann: Er weiß, wie die Prozessor-Pipeline funktioniert, er kann versuchen, Anweisungen schneller als ein Mensch neu zu ordnen, und so weiter - im Grunde ist es dasselbe wie Ein Computer ist so gut oder besser als der beste menschliche Spieler für Brettspiele usw., einfach weil er die Suche im Problemraum schneller machen kann als die meisten Menschen. Obwohl Sie theoretisch in einem bestimmten Fall genauso gut arbeiten können wie der Computer, können Sie dies sicherlich nicht mit der gleichen Geschwindigkeit tun, was es für mehr als einige Fälle unmöglich macht (dh der Compiler wird Sie mit Sicherheit übertreffen, wenn Sie versuchen zu schreiben mehr als ein paar Routinen im Assembler).
Auf der anderen Seite gibt es Fälle, in denen der Compiler nicht so viele Informationen hat - ich würde sagen, vor allem, wenn mit verschiedenen Formen externer Hardware gearbeitet wird, von denen der Compiler keine Kenntnis hat. Das Hauptbeispiel sind wahrscheinlich Gerätetreiber, bei denen Assembler in Kombination mit dem genauen Wissen eines Menschen über die betreffende Hardware bessere Ergebnisse erzielen können als ein C-Compiler.
Andere haben spezielle Anweisungen erwähnt, wovon ich im obigen Absatz spreche - Anweisungen, über die der Compiler möglicherweise nur begrenzte oder gar keine Kenntnisse hat, sodass ein Mensch schneller Code schreiben kann.
quelle
ocamlopt
überspringt der Compiler für nativen Code die Befehlsplanung auf x86 und überlässt es stattdessen der CPU, da er zur Laufzeit effektiver nachbestellen kann.In meinem Beruf gibt es drei Gründe, warum ich die Montage kenne und benutze. Der Wichtigkeit nach geordnet:
Debuggen - Ich erhalte häufig Bibliothekscode mit Fehlern oder unvollständiger Dokumentation. Ich finde heraus, was es tut, indem ich auf Baugruppenebene einspringe. Ich muss das ungefähr einmal pro Woche machen. Ich verwende es auch als Tool zum Debuggen von Problemen, bei denen meine Augen den idiomatischen Fehler in C / C ++ / C # nicht erkennen. Ein Blick auf die Baugruppe kommt darüber hinaus.
Optimieren - Der Compiler kann ziemlich gut optimieren, aber ich spiele in einem anderen Stadion als die meisten anderen. Ich schreibe Bildverarbeitungscode, der normalerweise mit Code beginnt, der so aussieht:
Das "etwas tun" geschieht typischerweise in der Größenordnung von mehreren Millionen Mal (dh zwischen 3 und 30). Durch das Abschaben von Zyklen in dieser Phase "etwas tun" werden die Leistungssteigerungen enorm vergrößert. Normalerweise beginne ich dort nicht - ich beginne normalerweise damit, zuerst den Code zu schreiben, um zu funktionieren, und dann mein Bestes zu geben, um das C so umzugestalten, dass es von Natur aus besser ist (besserer Algorithmus, weniger Last in der Schleife usw.). Normalerweise muss ich Assembly lesen, um zu sehen, was los ist, und muss es selten schreiben. Ich mache das vielleicht alle zwei oder drei Monate.
etwas zu tun, was die Sprache nicht zulässt. Dazu gehören - Abrufen der Prozessorarchitektur und spezifischer Prozessorfunktionen, Zugreifen auf Flags, die nicht in der CPU enthalten sind (Mann, ich wünschte wirklich, C hätte Ihnen Zugriff auf das Carry-Flag gewährt) usw. Ich mache dies möglicherweise einmal im Jahr oder zwei Jahre.
quelle
Nur wenn einige spezielle Befehlssätze verwendet werden, unterstützt der Compiler diese nicht.
Um die Rechenleistung einer modernen CPU mit mehreren Pipelines und vorausschauender Verzweigung zu maximieren, müssen Sie das Assembly-Programm so strukturieren, dass es a) für einen Menschen fast unmöglich zu schreiben ist, b) noch unmöglicher zu warten ist.
Bessere Algorithmen, Datenstrukturen und Speicherverwaltung bieten Ihnen mindestens eine Größenordnung mehr Leistung als die Mikrooptimierungen, die Sie bei der Montage durchführen können.
quelle
Obwohl C der Manipulation von 8-Bit-, 16-Bit-, 32-Bit- und 64-Bit-Daten auf niedriger Ebene "nahe" ist, gibt es einige mathematische Operationen, die von C nicht unterstützt werden und in bestimmten Montageanweisungen häufig elegant ausgeführt werden können Sätze:
Festkommamultiplikation: Das Produkt zweier 16-Bit-Zahlen ist eine 32-Bit-Zahl. Die Regeln in C besagen jedoch, dass das Produkt aus zwei 16-Bit-Zahlen eine 16-Bit-Zahl und das Produkt aus zwei 32-Bit-Zahlen eine 32-Bit-Zahl ist - in beiden Fällen die untere Hälfte. Wenn Sie die obere Hälfte einer 16x16-Multiplikation oder einer 32x32-Multiplikation wünschen , müssen Sie Spiele mit dem Compiler spielen. Die allgemeine Methode besteht darin, auf eine Bitbreite zu konvertieren, die größer als erforderlich ist, zu multiplizieren, nach unten zu verschieben und zurückzusetzen:
In diesem Fall ist der Compiler möglicherweise klug genug, um zu wissen, dass Sie wirklich nur versuchen, die obere Hälfte einer 16x16-Multiplikation zu erhalten und mit der nativen 16x16-Multiplikation der Maschine das Richtige zu tun. Oder es kann dumm sein und einen Bibliotheksaufruf erfordern, um die 32x32-Multiplikation durchzuführen, was viel zu viel des Guten ist, weil Sie nur 16 Bit des Produkts benötigen - aber der C-Standard gibt Ihnen keine Möglichkeit, sich auszudrücken.
Bestimmte Bitverschiebungsvorgänge (Drehung / Übertragen):
Dies ist in C nicht allzu unelegant, aber wenn der Compiler nicht klug genug ist, um zu erkennen, was Sie tun, wird er eine Menge "unnötiger" Arbeit leisten. In vielen Assembler-Befehlssätzen können Sie mit dem Ergebnis im Übertragsregister nach links / rechts drehen oder verschieben, sodass Sie die obigen Schritte in 34 Anweisungen ausführen können: Laden Sie einen Zeiger auf den Anfang des Arrays, löschen Sie den Übertrag und führen Sie 32 8- aus. Bit-Rechtsverschiebung durch automatische Inkrementierung des Zeigers.
Für ein anderes Beispiel gibt es lineare Rückkopplungsschieberegister (LFSR), die in der Montage elegant ausgeführt werden: Nehmen Sie einen Teil von N Bits (8, 16, 32, 64, 128 usw.) und verschieben Sie das Ganze um 1 nach rechts (siehe oben) Algorithmus), wenn der resultierende Übertrag 1 ist, dann XOR Sie in einem Bitmuster, das das Polynom darstellt.
Trotzdem würde ich nicht auf diese Techniken zurückgreifen, wenn ich keine ernsthaften Leistungseinschränkungen hätte. Wie andere bereits gesagt haben, ist die Assembly viel schwieriger zu dokumentieren / zu debuggen / zu testen / zu warten als der C-Code: Der Leistungsgewinn ist mit einigen erheblichen Kosten verbunden.
Bearbeiten: 3. In der Baugruppe ist eine Überlauferkennung möglich (in C ist dies nicht möglich). Dies erleichtert einige Algorithmen erheblich.
quelle
Kurze Antwort? Manchmal.
Technisch gesehen hat jede Abstraktion Kosten und eine Programmiersprache ist eine Abstraktion für die Funktionsweise der CPU. C ist jedoch sehr nah. Ich erinnere mich, dass ich vor Jahren laut gelacht habe, als ich mich in meinem UNIX-Konto angemeldet und die folgende Glücksmeldung erhalten habe (als solche Dinge beliebt waren):
Es ist lustig, weil es wahr ist: C ist wie eine tragbare Assemblersprache.
Es ist erwähnenswert, dass die Assemblersprache nur ausgeführt wird, wie Sie sie schreiben. Es gibt jedoch einen Compiler zwischen C und der von ihm generierten Assemblersprache, und das ist äußerst wichtig, da die Geschwindigkeit Ihres C-Codes sehr viel damit zu tun hat, wie gut Ihr Compiler ist.
Als gcc auf die Bühne kam, war eines der Dinge, die es so beliebt machten, dass es oft so viel besser war als die C-Compiler, die mit vielen kommerziellen UNIX-Varianten ausgeliefert wurden. Es war nicht nur ANSI C (keiner dieser K & R C-Abfälle), es war auch robuster und produzierte normalerweise besseren (schnelleren) Code. Nicht immer aber oft.
Ich sage Ihnen das alles, weil es keine pauschale Regel für die Geschwindigkeit von C und Assembler gibt, weil es keinen objektiven Standard für C gibt.
Ebenso variiert der Assembler stark, je nachdem, welchen Prozessor Sie ausführen, welche Systemspezifikation Sie verwenden, welchen Befehlssatz Sie verwenden usw. In der Vergangenheit gab es zwei CPU-Architekturfamilien: CISC und RISC. Der größte Player in CISC war und ist die Intel x86-Architektur (und der Befehlssatz). RISC dominierte die UNIX-Welt (MIPS6000, Alpha, Sparc usw.). CISC hat den Kampf um Herz und Verstand gewonnen.
Wie auch immer, als ich ein jüngerer Entwickler war, war die populäre Weisheit, dass handgeschriebenes x86 oft viel schneller als C sein kann, weil die Architektur so funktioniert, dass sie eine Komplexität aufweist, die von einem Menschen profitiert, der es tut. RISC hingegen schien für Compiler konzipiert zu sein, so dass niemand (ich wusste) einen Sparc-Assembler schrieb. Ich bin mir sicher, dass es solche Leute gab, aber zweifellos sind sie beide verrückt geworden und inzwischen institutionalisiert.
Befehlssätze sind selbst in derselben Prozessorfamilie ein wichtiger Punkt. Bestimmte Intel-Prozessoren verfügen über Erweiterungen wie SSE bis SSE4. AMD hatte ihre eigenen SIMD-Anweisungen. Der Vorteil einer Programmiersprache wie C war, dass jemand seine Bibliothek schreiben konnte, sodass sie für jeden Prozessor optimiert war, auf dem Sie ausgeführt wurden. Das war harte Arbeit im Assembler.
Es gibt immer noch Optimierungen, die Sie in Assembler vornehmen können, die kein Compiler vornehmen kann, und ein gut geschriebener Assembler-Algorithmus ist genauso schnell oder schneller als sein C-Äquivalent. Die größere Frage ist: Lohnt es sich?
Letztendlich war Assembler jedoch ein Produkt seiner Zeit und zu einer Zeit populärer, als CPU-Zyklen teuer waren. Heutzutage kann eine CPU, deren Herstellung 5 bis 10 US-Dollar kostet (Intel Atom), so ziemlich alles, was sich jeder wünschen kann. Der einzige wirkliche Grund, Assembler heutzutage zu schreiben, sind Dinge auf niedriger Ebene wie einige Teile eines Betriebssystems (obwohl die überwiegende Mehrheit des Linux-Kernels in C geschrieben ist), Gerätetreiber und möglicherweise eingebettete Geräte (obwohl C dort tendenziell dominiert) auch) und so weiter. Oder nur für Tritte (was etwas masochistisch ist).
quelle
Ein Anwendungsfall, der möglicherweise nicht mehr gilt, aber für Ihr Nerd-Vergnügen: Auf dem Amiga kämpfen die CPU und die Grafik- / Audio-Chips um den Zugriff auf einen bestimmten RAM-Bereich (die ersten 2 MB RAM, um genau zu sein). Wenn Sie also nur 2 MB RAM (oder weniger) hätten, würde die Anzeige komplexer Grafiken und die Wiedergabe von Sound die Leistung der CPU beeinträchtigen.
In Assembler können Sie Ihren Code so clever verschachteln, dass die CPU nur dann versucht, auf den RAM zuzugreifen, wenn die Grafik- / Audio-Chips intern ausgelastet sind (dh wenn der Bus frei ist). Wenn Sie also Ihre Anweisungen neu anordnen, den CPU-Cache und das Bus-Timing geschickt verwenden, können Sie einige Effekte erzielen, die mit einer höheren Sprache einfach nicht möglich waren, da Sie jeden Befehl zeitlich festlegen und sogar hier und da NOPs einfügen mussten, um die verschiedenen zu behalten Chips aus dem Radar des anderen.
Dies ist ein weiterer Grund, warum die NOP-Anweisung (No Operation - do nothing) der CPU dazu führen kann, dass Ihre gesamte Anwendung schneller ausgeführt wird.
[BEARBEITEN] Natürlich hängt die Technik von einem bestimmten Hardware-Setup ab. Dies war der Hauptgrund, warum viele Amiga-Spiele mit schnelleren CPUs nicht umgehen konnten: Das Timing der Anweisungen war falsch.
quelle
Punkt eins, der nicht die Antwort ist.
Selbst wenn Sie nie darin programmieren, finde ich es nützlich, mindestens einen Assembler-Befehlssatz zu kennen. Dies ist Teil der unendlichen Suche der Programmierer, mehr zu wissen und daher besser zu werden. Auch nützlich, wenn Sie in Frameworks eintreten, für die Sie keinen Quellcode haben und zumindest eine ungefähre Vorstellung davon haben, was los ist. Es hilft Ihnen auch, JavaByteCode und .Net IL zu verstehen, da beide Assembler ähnlich sind.
Beantwortung der Frage, wenn Sie wenig oder viel Zeit haben. Am nützlichsten für die Verwendung in eingebetteten Chips, bei denen eine geringe Chipkomplexität und eine schlechte Konkurrenz bei Compilern, die auf diese Chips abzielen, das Gleichgewicht zugunsten des Menschen beeinflussen können. Auch bei eingeschränkten Geräten tauschen Sie häufig die Codegröße / Speichergröße / Leistung auf eine Weise aus, zu der ein Compiler nur schwer angewiesen werden kann. Ich weiß beispielsweise, dass diese Benutzeraktion nicht oft aufgerufen wird, sodass ich eine kleine Codegröße und eine schlechte Leistung habe. Diese andere Funktion, die ähnlich aussieht, wird jedoch jede Sekunde verwendet, damit ich eine größere Codegröße und eine schnellere Leistung habe. Dies ist die Art von Kompromiss, die ein erfahrener Montageprogrammierer eingehen kann.
Ich möchte auch hinzufügen, dass es viele Mittelwege gibt, auf denen Sie in C kompilieren und die erzeugte Assembly untersuchen und dann entweder Ihren C-Code ändern oder als Assembly optimieren und pflegen können.
Mein Freund arbeitet an Mikrocontrollern, derzeit Chips zur Steuerung kleiner Elektromotoren. Er arbeitet in einer Kombination aus Low Level C und Assembly. Er erzählte mir einmal von einem guten Arbeitstag, an dem er die Hauptschleife von 48 Anweisungen auf 43 reduziert hat. Er steht auch vor Entscheidungen, wie der Code gewachsen ist, um den 256k-Chip zu füllen, und das Unternehmen eine neue Funktion wünscht, oder?
Ich möchte als kommerzieller Entwickler mit einem ganzen Portfolio oder Sprachen, Plattformen und Arten von Anwendungen hinzufügen, bei denen ich noch nie das Bedürfnis hatte, in das Schreiben von Assembles einzutauchen. Ich habe jedoch immer das Wissen geschätzt, das ich darüber gewonnen habe. Und manchmal darin debuggt.
Ich weiß, dass ich die Frage "Warum sollte ich Assembler lernen?" Viel besser beantwortet habe, aber ich denke, es ist eine wichtigere Frage als wann sie schneller ist.
Versuchen wir es noch einmal. Sie sollten über die Montage nachdenken
Denken Sie daran, Ihre Assembly mit dem generierten Compiler zu vergleichen, um festzustellen, welche schneller / kleiner / besser ist.
David.
quelle
sbi
undcbi
), die Compiler aufgrund ihrer begrenzten Kenntnisse der Hardware früher (und manchmal immer noch) nicht voll ausnutzen.Ich bin überrascht, dass das niemand gesagt hat. Die
strlen()
Funktion ist viel schneller, wenn sie in Assembly geschrieben wird! In C ist das Beste, was Sie tun könnenWährend der Montage können Sie dies erheblich beschleunigen:
die länge ist in ecx. Dadurch werden 4 Zeichen gleichzeitig verglichen, sodass es 4-mal schneller ist. Und denken Sie, wenn Sie das Wort höherer Ordnung von eax und ebx verwenden, wird es achtmal schneller als die vorherige C-Routine!
quelle
(word & 0xFEFEFEFF) & (~word + 0x80808080)
Null basiert, wenn alle Bytes im Wort ungleich Null sind.Matrixoperationen mit SIMD-Anweisungen sind wahrscheinlich schneller als vom Compiler generierter Code.
quelle
Ich kann die spezifischen Beispiele nicht nennen, weil es zu viele Jahre her ist, aber es gab viele Fälle, in denen handgeschriebene Assembler jeden Compiler übertreffen konnten. Gründe warum:
Sie können davon abweichen, Konventionen aufzurufen und Argumente in Registern zu übergeben.
Sie könnten sorgfältig überlegen, wie Register verwendet werden sollen, und vermeiden, Variablen im Speicher zu speichern.
Bei Dingen wie Sprungtabellen können Sie vermeiden, dass Sie den Index auf Grenzen überprüfen müssen.
Grundsätzlich optimieren Compiler ziemlich gut, und das ist fast immer "gut genug", aber in einigen Situationen (wie dem Rendern von Grafiken), in denen Sie für jeden einzelnen Zyklus teuer bezahlen, können Sie Verknüpfungen verwenden, weil Sie den Code kennen , wo ein Compiler nicht konnte, weil er auf der sicheren Seite sein muss.
Tatsächlich habe ich von einem Grafik-Rendering-Code gehört, bei dem eine Routine, wie eine Routine zum Zeichnen von Linien oder zum Füllen von Polygonen, tatsächlich einen kleinen Block Maschinencode auf dem Stapel generiert und dort ausgeführt hat, um eine kontinuierliche Entscheidungsfindung zu vermeiden über Linienstil, Breite, Muster usw.
Das heißt, ich möchte, dass ein Compiler guten Assembler-Code für mich generiert, aber nicht zu schlau ist, und das tun sie meistens. Tatsächlich ist eines der Dinge, die ich an Fortran hasse, das Verwürfeln des Codes, um ihn zu "optimieren", normalerweise ohne nennenswerten Zweck.
Wenn Apps Leistungsprobleme haben, liegt dies normalerweise an verschwenderischem Design. Heutzutage würde ich Assembler niemals für die Leistung empfehlen, es sei denn, die gesamte App wurde bereits innerhalb eines Zentimeters ihres Lebens optimiert, war immer noch nicht schnell genug und verbrachte ihre ganze Zeit in engen inneren Schleifen.
Hinzugefügt: Ich habe viele Apps gesehen, die in Assemblersprache geschrieben wurden, und der Hauptvorteil der Geschwindigkeit gegenüber einer Sprache wie C, Pascal, Fortran usw. war, dass der Programmierer beim Codieren in Assembler weitaus vorsichtiger war. Er oder sie wird ungefähr 100 Codezeilen pro Tag schreiben, unabhängig von der Sprache und in einer Compilersprache, die 3 oder 400 Anweisungen entspricht.
quelle
Einige Beispiele aus meiner Erfahrung:
Zugriff auf Anweisungen, auf die von C aus nicht zugegriffen werden kann Beispielsweise unterstützen viele Architekturen (wie x86-64, IA-64, DEC Alpha und 64-Bit-MIPS oder PowerPC) eine 64-Bit-64-Bit-Multiplikation, die ein 128-Bit-Ergebnis ergibt. GCC hat kürzlich eine Erweiterung hinzugefügt, die den Zugriff auf solche Anweisungen ermöglicht, jedoch bevor diese Assembly erforderlich war. Der Zugriff auf diese Anweisung kann bei 64-Bit-CPUs bei der Implementierung von RSA einen großen Unterschied bewirken - manchmal sogar um den Faktor 4 der Leistungsverbesserung.
Zugriff auf CPU-spezifische Flags. Derjenige, der mich sehr gebissen hat, ist die Tragflagge; Wenn Sie bei einer Addition mit mehrfacher Genauigkeit keinen Zugriff auf das CPU-Übertragsbit haben, müssen Sie stattdessen das Ergebnis vergleichen, um festzustellen, ob es übergelaufen ist. Dies erfordert 3-5 weitere Anweisungen pro Glied. und schlimmer noch, die in Bezug auf Datenzugriffe ziemlich seriell sind, was die Leistung moderner superskalarer Prozessoren beeinträchtigt. Wenn Tausende solcher Ganzzahlen hintereinander verarbeitet werden, ist die Verwendung von addc ein großer Gewinn (es gibt auch superskalare Probleme mit Konflikten um das Übertragsbit, aber moderne CPUs kommen ziemlich gut damit zurecht).
SIMD. Selbst Autovectorizing-Compiler können nur relativ einfache Fälle ausführen. Wenn Sie also eine gute SIMD-Leistung wünschen, müssen Sie den Code leider häufig direkt schreiben. Natürlich können Sie Intrinsics anstelle von Assembly verwenden, aber sobald Sie sich auf der Intrinsics-Ebene befinden, schreiben Sie ohnehin Assembly, indem Sie den Compiler nur als Registerzuweiser und (nominell) Befehlsplaner verwenden. (Ich neige dazu, Intrinsics für SIMD zu verwenden, nur weil der Compiler die Funktionsprologe und so weiter für mich generieren kann, sodass ich unter Linux, OS X und Windows denselben Code verwenden kann, ohne mich mit ABI-Problemen wie Funktionsaufrufkonventionen, aber anderen befassen zu müssen als das sind die SSE-Intrinsics wirklich nicht sehr schön - die Altivec-Intrinsics scheinen besser zu sein, obwohl ich nicht viel Erfahrung mit ihnen habe).Bitslicing AES- oder SIMD-Fehlerkorrektur - man könnte sich einen Compiler vorstellen, der Algorithmen analysieren und solchen Code generieren könnte, aber ich denke , ein solcher intelligenter Compiler ist mindestens 30 Jahre von der Existenz entfernt (bestenfalls).
Auf der anderen Seite haben Multicore-Maschinen und verteilte Systeme viele der größten Leistungsgewinne in die andere Richtung verschoben - erhalten Sie eine zusätzliche Beschleunigung von 20% beim Schreiben Ihrer inneren Schleifen in der Baugruppe oder 300% durch Ausführen über mehrere Kerne oder 10000% durch Ausführen über einen Cluster von Computern. Und natürlich sind Optimierungen auf hoher Ebene (Dinge wie Futures, Memoization usw.) in einer höheren Sprache wie ML oder Scala als C oder asm oft viel einfacher durchzuführen und können oft zu einem viel größeren Leistungsgewinn führen. Wie immer müssen also Kompromisse geschlossen werden.
quelle
Enge Schleifen, wie beim Spielen mit Bildern, da ein Bild aus Millionen von Pixeln bestehen kann. Sich hinzusetzen und herauszufinden, wie die begrenzte Anzahl von Prozessorregistern optimal genutzt werden kann, kann einen Unterschied machen. Hier ist ein Beispiel aus dem wirklichen Leben:
http://danbystrom.se/2008/12/22/optimizing-away-ii/
Dann haben Prozessoren oft einige esoterische Anweisungen, die für einen Compiler zu spezialisiert sind, um sie zu bearbeiten, aber gelegentlich kann ein Assembler-Programmierer sie gut nutzen. Nehmen Sie zum Beispiel die XLAT-Anweisung. Wirklich großartig, wenn Sie Tischsuchen in einer Schleife durchführen müssen und die Tabelle auf 256 Bytes begrenzt ist!
Aktualisiert: Oh, denken Sie nur daran, was am wichtigsten ist, wenn wir allgemein von Schleifen sprechen: Der Compiler hat oft keine Ahnung, wie viele Iterationen dies häufig sein werden! Nur der Programmierer weiß, dass eine Schleife VIELE Male wiederholt wird und dass es daher vorteilhaft ist, sich mit etwas zusätzlicher Arbeit auf die Schleife vorzubereiten, oder dass sie so oft wiederholt wird, dass die Einrichtung tatsächlich länger dauert als die Iterationen erwartet.
quelle
Öfter als Sie denken, muss C Dinge tun, die aus Sicht eines Assembly-Codierers unnötig erscheinen, nur weil die C-Standards dies vorschreiben.
Ganzzahlige Promotion zum Beispiel. Wenn Sie eine char-Variable in C verschieben möchten, würde man normalerweise erwarten, dass der Code tatsächlich genau das tut, eine einzelne Bitverschiebung.
Die Standards erzwingen jedoch, dass der Compiler vor der Verschiebung ein Vorzeichen auf int erweitert und das Ergebnis anschließend auf char abschneidet, was den Code abhängig von der Architektur des Zielprozessors komplizieren kann.
quelle
Sie wissen nicht wirklich, ob Ihr gut geschriebener C-Code wirklich schnell ist, wenn Sie sich nicht die Demontage dessen angesehen haben, was der Compiler produziert. Oft schaut man es sich an und sieht, dass "gut geschrieben" subjektiv war.
Es ist also nicht notwendig, in Assembler zu schreiben, um den schnellsten Code aller Zeiten zu erhalten, aber es lohnt sich auf jeden Fall, Assembler aus dem gleichen Grund zu kennen.
quelle
Ich habe alle Antworten lesen (mehr als 30) und nicht einen einfachen Grund gefunden: Assembler schneller als C ist , wenn Sie gelesen haben und die Intel® 64 und IA-32 Architektur - Optimierung Referenzhandbuch , so der Grund , warum Montage kann Langsamer ist, dass Leute, die solch eine langsamere Assembly schreiben, das Optimierungshandbuch nicht gelesen haben .
In den guten alten Zeiten von Intel 80286 wurde jeder Befehl mit einer festen Anzahl von CPU-Zyklen ausgeführt, aber seit Pentium Pro, das 1995 veröffentlicht wurde, wurden Intel-Prozessoren superskalar und verwendeten Complex Pipelining: Out-of-Order Execution & Register Renaming. Zuvor gab es auf Pentium, das 1993 hergestellt wurde, U- und V-Pipelines: Doppelrohrleitungen, die zwei einfache Anweisungen in einem Taktzyklus ausführen konnten, wenn sie nicht voneinander abhängig waren; Dies war jedoch nichts Vergleichbares zu dem, was in Pentium Pro als Out-of-Order Execution & Register Renaming erschien und heutzutage fast unverändert blieb.
Um es in wenigen Worten zu erklären: Im schnellsten Code hängen Anweisungen nicht von vorherigen Ergebnissen ab, z. B. sollten Sie immer ganze Register löschen (von movzx) oder
add rax, 1
stattdessen oder verwendeninc rax
die Abhängigkeit vom vorherigen Status von Flags usw. entfernen.Wenn es die Zeit erlaubt, können Sie mehr über Out-of-Order-Ausführung und Umbenennung von Registern lesen. Im Internet sind zahlreiche Informationen verfügbar.
Es gibt auch andere wichtige Probleme wie die Verzweigungsvorhersage, die Anzahl der Lade- und Speichereinheiten, die Anzahl der Gates, die Mikrooperationen ausführen usw., aber das Wichtigste, das berücksichtigt werden muss, ist die Ausführung außerhalb der Reihenfolge.
Die meisten Leute sind sich der Ausführung außerhalb der Reihenfolge einfach nicht bewusst, daher schreiben sie ihre Assembly-Programme wie für 80286 und erwarten, dass die Ausführung ihrer Anweisung unabhängig vom Kontext eine feste Zeit in Anspruch nimmt. C-Compiler sind sich der Ausführung außerhalb der Reihenfolge bewusst und generieren den Code korrekt. Das ist der Grund, warum der Code solcher ahnungsloser Personen langsamer ist. Wenn Sie jedoch darauf aufmerksam werden, ist Ihr Code schneller.
quelle
Ich denke, der allgemeine Fall, wenn Assembler schneller ist, ist, wenn ein intelligenter Assembler-Programmierer die Ausgabe des Compilers betrachtet und sagt, "dies ist ein kritischer Pfad für die Leistung, und ich kann dies schreiben, um effizienter zu sein", und dann diese Person diesen Assembler optimiert oder neu schreibt von Grund auf neu.
quelle
Es hängt alles von Ihrer Arbeitsbelastung ab.
Für den täglichen Betrieb sind C und C ++ in Ordnung, aber es gibt bestimmte Workloads (alle Transformationen mit Video (Komprimierung, Dekomprimierung, Bildeffekte usw.)), für deren Ausführung die Montage ziemlich genau erforderlich ist.
Dazu gehören normalerweise auch CPU-spezifische Chipsatz-Erweiterungen (MME / MMX / SSE / was auch immer), die auf diese Art von Betrieb abgestimmt sind.
quelle
Ich habe eine Operation der Transposition von Bits, die durchgeführt werden muss, bei 192 oder 256 Bit bei jedem Interrupt, der alle 50 Mikrosekunden auftritt.
Dies geschieht durch eine feste Zuordnung (Hardwareeinschränkungen). Bei Verwendung von C dauerte die Herstellung etwa 10 Mikrosekunden. Als ich dies in Assembler übersetzte, berücksichtigte ich die spezifischen Merkmale dieser Zuordnung, das spezifische Zwischenspeichern von Registern und die Verwendung bitorientierter Operationen. Die Leistung dauerte weniger als 3,5 Mikrosekunden.
quelle
Es könnte sich lohnen, Optimizing Immutable and Purity von Walter Bright zu betrachten. Es ist kein profilierter Test, sondern zeigt Ihnen ein gutes Beispiel für einen Unterschied zwischen handgeschriebenem und vom Compiler generiertem ASM. Walter Bright schreibt optimierende Compiler, sodass es sich lohnen könnte, sich seine anderen Blog-Beiträge anzusehen.
quelle
LInux Assembly Howto , stellt diese Frage und gibt die Vor- und Nachteile der Verwendung von Assembly.
quelle
Die einfache Antwort ... Wer sich mit Assembly gut auskennt (auch bekannt als Referenz) und jeden kleinen Prozessor-Cache, jede Pipeline-Funktion usw. nutzt, kann garantiert viel schnelleren Code produzieren als jeder Compiler.
Allerdings spielt der Unterschied heutzutage in der typischen Anwendung keine Rolle.
quelle
Eine der Möglichkeiten für die CP / M-86-Version von PolyPascal (Geschwister von Turbo Pascal) bestand darin, die Funktion "Bios für die Ausgabe von Zeichen auf dem Bildschirm verwenden" durch eine Routine in Maschinensprache zu ersetzen, die im Wesentlichen vorhanden ist wurde das x und y und die Zeichenfolge gegeben, um dort zu setzen.
Dadurch konnte der Bildschirm viel, viel schneller als zuvor aktualisiert werden!
In der Binärdatei war Platz zum Einbetten von Maschinencode (einige hundert Bytes), und es gab auch andere Dinge, daher war es wichtig, so viel wie möglich zusammenzudrücken.
Es stellte sich heraus, dass beide Koordinaten in ein Byte passen konnten, da der Bildschirm 80 x 25 groß war, sodass beide in ein Zwei-Byte-Wort passen konnten. Dies ermöglichte es, die erforderlichen Berechnungen in weniger Bytes durchzuführen, da eine einzelne Addition beide Werte gleichzeitig manipulieren konnte.
Meines Wissens gibt es keine C-Compiler, die mehrere Werte in einem Register zusammenführen, SIMD-Anweisungen ausführen und sie später erneut aufteilen können (und ich glaube nicht, dass die Maschinenanweisungen sowieso kürzer sein werden).
quelle
Einer der bekanntesten Assemblierungsausschnitte stammt aus Michael Abrashs Textur-Mapping-Schleife ( hier ausführlich erläutert ):
Heutzutage drücken die meisten Compiler erweiterte CPU-spezifische Anweisungen als intrinsische Funktionen aus, dh Funktionen, die bis zur eigentlichen Anweisung kompiliert werden. MS Visual C ++ unterstützt Intrinsics für MMX, SSE, SSE2, SSE3 und SSE4, sodass Sie sich weniger Gedanken über das Herunterfallen auf die Assembly machen müssen, um die plattformspezifischen Anweisungen nutzen zu können. Visual C ++ kann auch die tatsächliche Architektur nutzen, auf die Sie mit der entsprechenden / ARCH-Einstellung abzielen.
quelle
Mit dem richtigen Programmierer können Assembler-Programme immer schneller als ihre C-Gegenstücke erstellt werden (zumindest geringfügig). Es wäre schwierig, ein C-Programm zu erstellen, in dem Sie nicht mindestens eine Anweisung des Assemblers ausführen können.
quelle
http://cr.yp.to/qhasm.html enthält viele Beispiele.
quelle
gcc ist zu einem weit verbreiteten Compiler geworden. Die Optimierungen sind im Allgemeinen nicht so gut. Weitaus besser als der durchschnittliche Programmierer, der Assembler schreibt, aber für echte Leistung nicht so gut. Es gibt Compiler, deren Code einfach unglaublich ist. Als allgemeine Antwort wird es also viele Stellen geben, an denen Sie in die Ausgabe des Compilers gehen und den Assembler für die Leistung optimieren und / oder die Routine einfach von Grund auf neu schreiben können.
quelle
Longpoke, es gibt nur eine Einschränkung: Zeit. Wenn Sie nicht über die Ressourcen verfügen, um jede einzelne Änderung des Codes zu optimieren und Ihre Zeit mit der Zuweisung von Registern zu verbringen, einige Verschüttungen zu optimieren und was nicht, gewinnt der Compiler jedes Mal. Sie ändern den Code, kompilieren ihn neu und messen ihn. Bei Bedarf wiederholen.
Auch auf hoher Ebene kann man viel machen. Wenn Sie die resultierende Baugruppe überprüfen, kann dies den Eindruck erwecken, dass der Code Mist ist. In der Praxis wird er jedoch schneller ausgeführt, als Sie es für schneller halten. Beispiel:
int y = Daten [i]; // mach hier ein paar Sachen .. call_function (y, ...);
Der Compiler liest die Daten, schiebt sie in den Stapel (Spill) und liest sie später aus dem Stapel und übergibt sie als Argument. Klingt scheiße? Dies kann tatsächlich eine sehr effektive Latenzkompensation sein und zu einer schnelleren Laufzeit führen.
// optimierte Version call_function (data [i], ...); // doch nicht so optimiert ..
Die Idee mit der optimierten Version war, dass wir den Registerdruck reduziert und das Verschütten vermieden haben. Aber in Wahrheit war die "beschissene" Version schneller!
Ein Blick auf den Assembler-Code, ein Blick auf die Anweisungen und die Schlussfolgerung: Mehr Anweisungen, langsamer, wäre eine Fehleinschätzung.
Hier ist zu beachten: Viele Montageexperten glauben , viel zu wissen, wissen aber nur sehr wenig. Die Regeln ändern sich auch von Architektur zu Architektur. Es gibt zum Beispiel keinen Silver-Bullet-x86-Code, der immer der schnellste ist. Heutzutage ist es besser, sich an Faustregeln zu halten:
Zu viel Vertrauen in den Compiler zu haben, um schlecht durchdachten C / C ++ - Code auf magische Weise in "theoretisch optimalen" Code umzuwandeln, ist Wunschdenken. Sie müssen den Compiler und die Toolkette kennen, die Sie verwenden, wenn Sie sich für "Leistung" auf dieser niedrigen Ebene interessieren.
Compiler in C / C ++ sind im Allgemeinen nicht sehr gut darin, Unterausdrücke neu zu ordnen, da die Funktionen für den Anfang Nebenwirkungen haben. Funktionale Sprachen leiden nicht unter dieser Einschränkung, passen aber nicht so gut zum aktuellen Ökosystem. Es gibt Compileroptionen, mit denen entspannte Genauigkeitsregeln ermöglicht werden, mit denen die Reihenfolge der Operationen vom Compiler / Linker / Codegenerator geändert werden kann.
Dieses Thema ist eine Sackgasse. Für die meisten ist es nicht relevant, und die anderen wissen sowieso schon, was sie tun.
Alles läuft darauf hinaus: "Um zu verstehen, was Sie tun", ist es ein bisschen anders als zu wissen, was Sie tun.
quelle