502 Stimmen

Wann ist Assembler schneller als C?

Einer der angegebenen Gründe für Assembler-Kenntnisse ist, dass man damit gelegentlich Code schreiben kann, der leistungsfähiger ist als der Code in einer höheren Sprache, insbesondere C. Ich habe aber auch schon oft gehört, dass, obwohl das nicht ganz falsch ist, die Fälle, in denen Assembler helfen kann eigentlich zur Generierung von leistungsfähigerem Code verwendet werden können, sind extrem selten und erfordern Expertenwissen und Erfahrung mit Assembler.

Diese Frage geht noch nicht einmal auf die Tatsache ein, dass Assembler-Anweisungen maschinenspezifisch und nicht portierbar sind, oder auf andere Aspekte von Assembler. Es gibt viele gute Gründe, Assembler zu kennen, abgesehen von diesem, natürlich, aber dies soll eine spezifische Frage sein, die nach Beispielen und Daten fragt, nicht ein ausgedehnter Diskurs über Assembler gegenüber höheren Sprachen.

Kann jemand etwas über spezifische Beispiele in welchen Fällen Assembler schneller ist als gut geschriebener C-Code mit einem modernen Compiler, und können Sie diese Behauptung mit Profilergebnissen belegen? Ich bin mir ziemlich sicher, dass es diese Fälle gibt, aber ich möchte wirklich genau wissen, wie esoterisch diese Fälle sind, da dies ein Punkt zu sein scheint, über den man streiten kann.

0 Stimmen

Und nun wäre eine weitere Frage angebracht: Wann ist die Tatsache, dass Assembler schneller ist als C, tatsächlich von Bedeutung?

20 Stimmen

Eigentlich ist es recht trivial, kompilierten Code zu verbessern. Jeder, der über solide Kenntnisse in Assembler und C verfügt, kann dies erkennen, indem er den erzeugten Code untersucht. Eine einfache Möglichkeit ist die erste Leistungsklippe, von der man herunterfällt, wenn man in der kompilierten Version keine freien Register mehr hat. Im Durchschnitt wird der Compiler bei einem großen Projekt weitaus besser abschneiden als ein Mensch, aber es ist nicht schwer, bei einem Projekt von angemessener Größe Leistungsprobleme im kompilierten Code zu finden.

19 Stimmen

Die kurze Antwort lautet eigentlich: Assembler ist siempre Der Grund dafür ist, dass man Assembler ohne C haben kann, aber man kann C nicht ohne Assembler haben (in der binären Form, die wir früher "Maschinencode" nannten). Das heißt, die lange Antwort ist: C-Compiler sind ziemlich gut darin, zu optimieren und über Dinge "nachzudenken", an die man normalerweise nicht denkt, also hängt es wirklich von Ihren Fähigkeiten ab, aber normalerweise können Sie den C-Compiler immer schlagen; es ist immer noch nur eine Software, die nicht denken und Ideen bekommen kann. Sie können auch portablen Assembler schreiben, wenn Sie Makros verwenden und geduldig sind.

15voto

mmx Punkte 400975

Matrixoperationen mit SIMD-Befehlen sind wahrscheinlich schneller als vom Compiler erzeugter Code.

0 Stimmen

Einige Compiler (der VectorC, wenn ich mich recht erinnere) erzeugen SIMD-Code, so dass selbst das wahrscheinlich kein Argument mehr für die Verwendung von Assembler-Code ist.

0 Stimmen

Compiler erstellen SSE-fähigen Code, so dass dieses Argument nicht zutrifft.

0 Stimmen

Ja, moderne Compiler sind sich der SIMD-Befehle ziemlich bewusst. Um das Beste daraus zu machen, muss man den verwendeten Algorithmus auf hohem Niveau verstehen. Compiler erzeugen SIMD nur für offensichtliche Fälle. Die meisten von ihnen nehmen nicht Ihren gesamten Algorithmus und wandeln ihn in ein paralleles Äquivalent um.

15voto

Aaron Digulla Punkte 308693

Ein Anwendungsfall, der vielleicht nicht mehr zutrifft, aber für Ihr Nerd-Vergnügen: Auf dem Amiga kämpften die CPU und die Grafik-/Audio-Chips um den Zugriff auf einen bestimmten Bereich des Arbeitsspeichers (die ersten 2MB des Arbeitsspeichers, um genau zu sein). Wenn man also nur 2MB RAM (oder weniger) hatte, würde die Darstellung komplexer Grafiken und die Wiedergabe von Sound die Leistung der CPU zerstören.

In Assembler konnte man seinen Code so verschachteln, dass die CPU nur dann auf das RAM zugreifen würde, wenn die Grafik-/Audio-Chips intern beschäftigt waren (d.h. wenn der Bus frei war). Durch die Neuanordnung der Befehle, die geschickte Nutzung des CPU-Caches und das Bus-Timing konnte man also einige Effekte erzielen, die mit einer höheren Programmiersprache einfach nicht möglich waren, da man jeden Befehl zeitlich abstimmen und hier und da sogar NOPs einfügen musste, um die verschiedenen Chips voneinander fernzuhalten.

Das ist ein weiterer Grund, warum der NOP-Befehl (No Operation - do nothing) der CPU Ihre gesamte Anwendung schneller machen kann.

[EDIT] Natürlich hängt die Technik von einer bestimmten Hardwarekonfiguration ab. Das war der Hauptgrund, warum viele Amiga-Spiele nicht mit schnelleren CPUs zurechtkamen: Das Timing der Anweisungen stimmte nicht.

0 Stimmen

Der Amiga hatte keine 16 MB Chip-RAM, eher 512 kB bis 2 MB, je nach Chipsatz. Außerdem funktionierten viele Amiga-Spiele nicht mit schnelleren CPUs aufgrund von Techniken, wie Sie sie beschreiben.

1 Stimmen

@bk1e - Amiga produzierte eine große Auswahl an verschiedenen Computermodellen, der Amiga 500 wurde mit 512K Ram ausgeliefert und in meinem Fall auf 1Meg erweitert. amigahistory.co.uk/amiedevsys.html ist ein Amiga mit 128Meg Ram

0 Stimmen

@bk1e: Ich korrigiere mich. Mein Gedächtnis lässt mich vielleicht im Stich, aber war das Chip-RAM nicht auf den ersten 24-Bit-Adressraum (d.h. 16 MB) beschränkt? Und Fast wurde darüber gemappt?

14voto

Jack Lloyd Punkte 8056

Ein paar Beispiele aus meiner Erfahrung:

  • Zugriff auf Befehle, die von C aus nicht zugänglich sind. Zum Beispiel unterstützen viele Architekturen (wie x86-64, IA-64, DEC Alpha und 64-Bit MIPS oder PowerPC) eine 64-Bit-mal-64-Bit-Multiplikation, die ein 128-Bit-Ergebnis ergibt. GCC hat kürzlich eine Erweiterung hinzugefügt, die den Zugriff auf solche Befehle ermöglicht, aber davor war Assembler erforderlich. Und der Zugriff auf diese Anweisung kann auf 64-Bit-CPUs einen enormen Unterschied machen, wenn man etwas wie RSA implementiert - manchmal sogar eine Leistungssteigerung um den Faktor 4.

  • Zugriff auf CPU-spezifische Merker. Wenn man bei einer Multipräzisionsaddition keinen Zugriff auf das CPU-Übertragsbit hat, muss man stattdessen das Ergebnis vergleichen, um zu sehen, ob es übergelaufen ist, was 3 bis 5 Anweisungen mehr pro Glied erfordert; und schlimmer noch, die Datenzugriffe sind ziemlich seriell, was die Leistung auf modernen superskalaren Prozessoren beeinträchtigt. Bei der Verarbeitung von Tausenden solcher Ganzzahlen in einer Reihe ist die Möglichkeit, addc zu verwenden, ein enormer Gewinn (es gibt auch superskalare Probleme mit Konflikten beim Übertragsbit, aber moderne CPUs kommen damit recht gut zurecht).

  • SIMD. Selbst autovektorisierende Compiler können nur relativ einfache Fälle bearbeiten. Wenn Sie also eine gute SIMD-Leistung wünschen, müssen Sie den Code leider oft direkt schreiben. Natürlich kann man Intrinsics anstelle von Assembler verwenden, aber sobald man sich auf der Intrinsics-Ebene befindet, schreibt man im Grunde sowieso Assembler und verwendet den Compiler nur noch als Registerzuweiser und (nominell) Befehlsplaner. (Ich neige dazu, Intrinsics für SIMD zu verwenden, einfach weil der Compiler die Funktionsprologe und so weiter für mich generieren kann, so dass ich den gleichen Code unter Linux, OS X und Windows verwenden kann, ohne mich mit ABI-Problemen wie Funktionsaufrufkonventionen befassen zu müssen, aber abgesehen davon sind die SSE-Intrinsics wirklich nicht sehr schön - die Altivec-Intrinsics scheinen besser zu sein, obwohl ich nicht viel Erfahrung mit ihnen habe). Als Beispiele für Dinge, die ein (heutiger) vektorisierender Compiler nicht herausfinden kann, lesen Sie über Bitslicing AES o SIMD-Fehlerkorrektur - Man könnte sich einen Compiler vorstellen, der Algorithmen analysiert und solchen Code erzeugt, aber ich habe das Gefühl, dass ein solcher intelligenter Compiler (bestenfalls) noch mindestens 30 Jahre entfernt ist.

Andererseits haben Multicore-Maschinen und verteilte Systeme viele der größten Leistungsgewinne in die andere Richtung verlagert - Sie können Ihre inneren Schleifen um 20 % beschleunigen, wenn Sie sie in Assembler schreiben, oder um 300 %, wenn Sie sie auf mehreren Kernen ausführen, oder um 100 000 %, wenn Sie sie auf einem Cluster von Maschinen ausführen. Und natürlich sind Optimierungen auf hoher Ebene (Dinge wie Futures, Memoisierung usw.) in einer höheren Sprache wie ML oder Scala oft viel einfacher zu bewerkstelligen als in C oder asm, und können oft einen viel größeren Leistungsgewinn bringen. Es müssen also wie immer Kompromisse eingegangen werden.

0 Stimmen

Außerdem ist intrinsisch basierter SIMD-Code in der Regel weniger lesbar als derselbe in Assembler geschriebene Code: Ein Großteil des SIMD-Codes beruht auf impliziten Neuinterpretationen der Daten in den Vektoren, was mit den Datentypen, die Compiler-Intrinsics bereitstellen, nur schwer zu bewerkstelligen ist.

13voto

Mike Dunlavey Punkte 39339

Ich kann keine konkreten Beispiele nennen, weil es schon zu viele Jahre her ist, aber es gab viele Fälle, in denen ein handgeschriebener Assembler jeden Compiler übertreffen konnte. Die Gründe dafür:

  • Sie können von den Aufrufkonventionen abweichen und Argumente in Registern übergeben.

  • Sie könnten sorgfältig überlegen, wie Sie Register verwenden und die Speicherung von Variablen im Speicher vermeiden.

  • Für Dinge wie Sprungtabellen könnten Sie die Überprüfung der Grenzen des Indexes vermeiden.

Im Grunde genommen leisten Compiler ziemlich gute Arbeit bei der Optimierung, und das ist fast immer "gut genug", aber in einigen Situationen (z. B. beim Rendern von Grafiken), in denen jeder einzelne Zyklus teuer bezahlt wird, kann man Abkürzungen nehmen, weil man den Code kennt, was ein Compiler nicht kann, weil er auf Nummer sicher gehen muss.

Ich habe sogar schon von Grafik-Rendering-Code gehört, bei dem eine Routine, z. B. eine Routine zum Zeichnen von Linien oder zum Füllen von Polygonen, tatsächlich einen kleinen Block von Maschinencode auf dem Stapel erzeugt und dort ausgeführt hat, um ständige Entscheidungen über Linienstil, Breite, Muster usw. zu vermeiden.

Abgesehen davon möchte ich, dass ein Compiler guten Assemblercode für mich erzeugt, aber nicht zu clever ist, und das tun sie meistens. Eines der Dinge, die ich an Fortran hasse, ist, dass er den Code verschlüsselt, um ihn zu "optimieren", was in der Regel zu keinem nennenswerten Ergebnis führt.

Wenn Anwendungen Leistungsprobleme haben, ist das in der Regel auf ein verschwenderisches Design zurückzuführen. Heutzutage würde ich niemals Assembler für die Leistung empfehlen, es sei denn, die gesamte Anwendung wurde bereits auf Herz und Nieren geprüft, war immer noch nicht schnell genug und verbrachte ihre gesamte Zeit in engen inneren Schleifen.

Hinzu kommt: Ich habe viele Anwendungen gesehen, die in Assembler geschrieben wurden, und der Hauptgeschwindigkeitsvorteil gegenüber einer Sprache wie C, Pascal, Fortran usw. lag darin, dass der Programmierer bei der Programmierung in Assembler viel sorgfältiger war. Er oder sie wird ungefähr 100 Zeilen Code pro Tag schreiben, unabhängig von der Sprache, und in einer Compiler-Sprache wird das 3 oder 400 Anweisungen entsprechen.

8 Stimmen

+1: "Sie könnten von den Aufrufkonventionen abweichen". C/C++-Compiler neigen dazu, bei der Rückgabe mehrerer Werte zu versagen. Sie verwenden oft die Sret-Form, bei der der Aufrufer einen zusammenhängenden Block für eine Struktur alloziert und dem Aufrufer einen Verweis darauf übergibt, damit er ihn ausfüllen kann. Die Rückgabe mehrerer Werte in Registern ist um ein Vielfaches schneller.

1 Stimmen

@Jon: C/C++-Compiler können das sehr gut, wenn die Funktion inlined wird (nicht-inlined Funktionen müssen der ABI entsprechen, das ist keine Einschränkung von C und C++, sondern das Verknüpfungsmodell)

0 Stimmen

@BenVoigt: Hier ist ein Gegenbeispiel flyingfrogblog.blogspot.co.uk/2012/04/

12voto

mfro Punkte 3223

Häufiger als man denkt, muss C Dinge tun, die aus der Sicht eines Assembler-Programmierers unnötig erscheinen, nur weil die C-Standards dies vorschreiben.

Ganzzahlige Förderung, zum Beispiel. Wenn man in C eine Char-Variable verschieben möchte, würde man normalerweise erwarten, dass der Code genau das tut, nämlich eine Verschiebung um ein einziges Bit.

Die Standards zwingen den Compiler jedoch, vor der Verschiebung eine Vorzeichenerweiterung nach int vorzunehmen und das Ergebnis anschließend nach char abzuschneiden, was den Code je nach Architektur des Zielprozessors verkomplizieren kann.

0 Stimmen

Qualitativ hochwertige Compiler für kleine Mikrocomputer sind seit Jahren in der Lage, die Verarbeitung der oberen Teile von Werten in Fällen zu vermeiden, in denen dies keinen bedeutenden Einfluss auf die Ergebnisse haben könnte. Promotionsregeln verursachen zwar Probleme, aber meistens in Fällen, in denen ein Compiler keine Möglichkeit hat zu wissen, welche Eckfälle relevant sind und welche nicht.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X