Wann ist die Montage schneller als C?


475

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.


17
Eigentlich ist es ziemlich trivial, kompilierten Code zu verbessern. Jeder mit soliden Kenntnissen der Assemblersprache und C kann dies anhand des generierten Codes erkennen. Jede einfache ist die erste Performance-Klippe, von der Sie abfallen, wenn Ihnen in der kompilierten Version die Einwegregister ausgehen. Im Durchschnitt ist der Compiler für ein großes Projekt weitaus besser als ein Mensch, aber in einem Projekt mit angemessener Größe ist es nicht schwierig, Leistungsprobleme im kompilierten Code zu finden.
old_timer

14
Eigentlich lautet die kurze Antwort: Assembler ist immer schneller oder gleich der Geschwindigkeit von C. Der Grund ist, dass Sie Assemblierung ohne C haben können, aber Sie können C nicht ohne Assemblierung haben (in der binären Form, die wir in der alten haben Tage genannt "Maschinencode"). Die lange Antwort lautet jedoch: C-Compiler sind ziemlich gut darin, Dinge zu optimieren und darüber nachzudenken, an die Sie normalerweise nicht denken. Das hängt also 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 einen tragbaren Assembler schreiben, wenn Sie Makros verwenden und geduldig sind.

11
Ich stimme überhaupt nicht zu, dass Antworten auf diese Frage "meinungsbasiert" sein müssen - sie können durchaus objektiv sein - es ist nicht so etwas wie der Versuch, die Leistung der bevorzugten Haustiersprachen zu vergleichen, für die jede ihre Stärken und Nachteile hat. Hier geht es darum zu verstehen, wie weit Compiler uns bringen können und von welchem ​​Punkt aus es besser ist, zu übernehmen.
Jsbueno

21
Zu Beginn meiner Karriere schrieb ich viel C- und Mainframe-Assembler bei einem Softwareunternehmen. Einer meiner Kollegen war das, was ich als "Assembler-Purist" bezeichnen würde (alles musste Assembler sein), also wette ich, dass ich eine bestimmte Routine schreiben konnte, die in C schneller lief als das, was er in Assembler schreiben konnte. Ich habe gewonnen. Aber um das Ganze abzurunden, sagte ich ihm, nachdem ich gewonnen hatte, dass ich eine zweite Wette haben wollte - dass ich im Assembler etwas schneller schreiben könnte als das C-Programm, das ihn bei der vorherigen Wette geschlagen hat. Ich habe das auch gewonnen und bewiesen, dass das meiste davon mehr als alles andere auf die Fähigkeiten und Fertigkeiten des Programmierers zurückzuführen ist.
Valerie R.

3
Wenn Ihr Gehirn keine -O3Flagge hat, sollten Sie die Optimierung wahrscheinlich dem C-Compiler
überlassen

Antworten:


272

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


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:

// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
  long long a_long = a; // cast to 64 bit.

  long long product = a_long * b; // perform multiplication

  return (int) (product >> 16);  // shift by the fixed point bias
}

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:

int inline FixedPointMul (int a, int b)
{
    return (int) __ll_rshift(__emul(a,b),16);
}

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 popcntoder Bit-Scan den ersten oder letzten Satz Bit zu finden . (POSIX hat eine ffs()Funktion, aber die Semantik stimmt nicht mit x86 bsf/ überein bsr. 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 popcntBefehl kompilieren (sofern dies zur Kompilierungszeit aktiviert ist). Die Verwendung __builtin_popcntin GNU C oder auf x86 ist jedoch viel zuverlässiger, wenn Sie nur sind Targeting-Hardware mit SSE4.2: _mm_popcnt_u32von<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 ntohlkann auf bswap(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.


6
Wie wäre es mit Dingen wie {x = c% d; y = c / d;}, sind Compiler klug genug, um daraus ein einzelnes div oder idiv zu machen?
Jens Björnhager

4
Tatsächlich würde ein guter Compiler den optimalen Code aus der ersten Funktion erzeugen. Es ist nicht das Beste, den Quellcode mit Intrinsics oder Inline-Assembly ohne jeglichen Nutzen zu verschleiern .
lockerer

65
Hallo Slacker, ich denke, Sie mussten noch nie an zeitkritischem Code arbeiten ... Inline-Assembly kann einen großen Unterschied machen. Auch für den Compiler ist eine Intrinsik dieselbe wie die normale Arithmetik in C. Das ist der Punkt in der Intrinsik. Mit ihnen können Sie eine Architekturfunktion verwenden, ohne sich mit den Nachteilen befassen zu müssen.
Nils Pipenbrinck

6
@slacker Eigentlich ist der Code hier gut lesbar: Der Inline-Code führt eine eindeutige Operation aus, die beim Lesen der Methodensignatur sofort verständlich ist. Der Code verliert nur langsam an Lesbarkeit, wenn eine obskure Anweisung verwendet wird. Was hier zählt, ist, dass wir eine Methode haben, die nur eine eindeutig identifizierbare Operation ausführt, und das ist wirklich der beste Weg, um lesbaren Code für diese atomaren Funktionen zu erzeugen. Übrigens ist dies nicht so dunkel, dass ein kleiner Kommentar wie / * (a * b) >> 16 * / ihn nicht sofort erklären kann.
Dereckson

5
Um fair zu sein, ist dieses Beispiel zumindest heute ein schlechtes. C-Compiler sind seit langem in der Lage, eine 32x32 -> 64-Multiplikation durchzuführen, auch wenn die Sprache dies nicht direkt anbietet: Sie erkennen, dass es nicht erforderlich ist, 32-Bit-Argumente in 64-Bit umzuwandeln und diese dann zu multiplizieren Führen Sie eine vollständige 64-Bit-Multiplikation durch, aber eine 32x32 -> 64 reicht völlig aus. Ich habe nachgesehen und alle Clang, Gcc und MSVC in ihrer aktuellen Version haben das richtig verstanden . Das ist nicht neu - ich erinnere mich, dass ich mir die Compiler-Ausgabe angesehen und dies vor einem Jahrzehnt bemerkt habe.
BeeOnRope

143

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.


3
Ja, es war ein Ein-Bit-Monochrom-System, insbesondere die Monochrom-Bildblöcke eines Atari ST.
Lilburne

16
Hat der optimierende Compiler das ursprüngliche Programm oder Ihre Version kompiliert?
Thorbjørn Ravn Andersen

Auf welchem ​​Prozessor? Bei 8086 würde ich erwarten, dass der optimale Code für eine 8x8-Drehung DI mit 16 Datenbits unter Verwendung von SI, Wiederholung add di,di / adc al,al / add di,di / adc ah,ahusw. 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.
Supercat

1
Ich kann mir wirklich keine Plattform vorstellen, auf der ein Compiler wahrscheinlich innerhalb eines oder zweier Faktoren des optimalen Codes für eine 8x8-Drehung landen würde.
Supercat

65

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):

#include "stdafx.h"
#include <windows.h>

float KahanSum(const float *data, int n)
{
   float sum = 0.0f, C = 0.0f, Y, T;

   for (int i = 0 ; i < n ; ++i) {
      Y = *data++ - C;
      T = sum + Y;
      C = T - sum - Y;
      sum = T;
   }

   return sum;
}

float AsmSum(const float *data, int n)
{
  float result = 0.0f;

  _asm
  {
    mov esi,data
    mov ecx,n
    fldz
    fldz
l1:
    fsubr [esi]
    add esi,4
    fld st(0)
    fadd st(0),st(2)
    fld st(0)
    fsub st(0),st(3)
    fsub st(0),st(2)
    fstp st(2)
    fstp st(2)
    loop l1
    fstp result
    fstp result
  }

  return result;
}

int main (int, char **)
{
  int count = 1000000;

  float *source = new float [count];

  for (int i = 0 ; i < count ; ++i) {
    source [i] = static_cast <float> (rand ()) / static_cast <float> (RAND_MAX);
  }

  LARGE_INTEGER start, mid, end;

  float sum1 = 0.0f, sum2 = 0.0f;

  QueryPerformanceCounter (&start);

  sum1 = KahanSum (source, count);

  QueryPerformanceCounter (&mid);

  sum2 = AsmSum (source, count);

  QueryPerformanceCounter (&end);

  cout << "  C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
  cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;

  return 0;
}

Und einige Zahlen von meinem PC, auf dem ein Standard-Release-Build * ausgeführt wird :

  C code: 500137 in 103884668
asm code: 500137 in 52129147

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.


20
Oder in GCC können Sie die Hände des Compilers für die Gleitkommaoptimierung (sofern Sie versprechen, nichts mit Unendlichkeiten oder NaNs zu tun) mithilfe des Flags lösen -ffast-math. Sie haben eine Optimierungsstufe, -Ofastdie 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).
David Stone

2
Ja, Floats sind nicht kommutativ, der Compiler muss genau das tun, was Sie geschrieben haben, im Grunde das, was @DavidStone gesagt hat.
Alec Teal

2
Hast du SSE Mathe ausprobiert? Die Leistung war einer der Gründe, warum MS x87 in x86_64 und 80-Bit-Double in x86 vollständig aufgegeben hat
phuclv

4
@Praxeolitic: FP add ist kommutativ ( 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 eine loopAnweisung eine großartige Demonstration von Fast Asm sind. loopist 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. Zwei fstp resultsInsns am Ende sind eindeutig nicht optimal. Das zusätzliche Ergebnis aus dem Stapel zu entfernen, wäre besser mit einem Nicht-Speicher. Wie fstp st(0)IIRC.
Peter Cordes

2
@PeterCordes: Eine interessante Konsequenz der kommutativen Addition ist, dass 0 + x und x + 0 zwar äquivalent zueinander sind, aber nicht immer gleich x.
Supercat

58

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.


Im Allgemeinen ist diese Aussage wahr. Der Compiler macht es am besten mit DWIW, aber in einigen Randfällen erledigt der Handcodierungs-Assembler die Arbeit, wenn Echtzeitleistung ein Muss ist.
Spoulson

1
@Liedman: "Es kann versuchen, Anweisungen schneller neu zu ordnen als ein Mensch". OCaml ist dafür bekannt, schnell zu sein. Überraschenderweise 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.
Jon Harrop

1
Moderne Compiler machen viel, und es würde viel zu lange dauern, sie von Hand zu machen, aber sie sind bei weitem nicht perfekt. Durchsuchen Sie die Bug-Tracker von gcc oder llvm nach "Missed-Optimization" -Fehlern. Da sind viele. Wenn Sie in asm schreiben, können Sie auch leichter Voraussetzungen wie "Diese Eingabe kann nicht negativ sein" nutzen, die für einen Compiler schwer zu beweisen wären.
Peter Cordes

48

In meinem Beruf gibt es drei Gründe, warum ich die Montage kenne und benutze. Der Wichtigkeit nach geordnet:

  1. 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.

  2. 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:

    for (int y=0; y < imageHeight; y++) {
        for (int x=0; x < imageWidth; x++) {
           // do something
        }
    }

    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.

  3. 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.


Sie kacheln Ihre Schleifen nicht? :-)
Jon Harrop

1
@plinth: wie meinst du "Scraping Cycles"?
Lang2

@ lang2: Es bedeutet, so viel überflüssige Zeit wie möglich in der inneren Schleife zu entfernen - alles, was der Compiler nicht herausholen konnte, einschließlich der Verwendung von Algebra, um eine Multiplikation aus einer Schleife herauszuheben, um sie zu einer Addition zu machen im Inneren usw.
Sockel

1
Schleifenkacheln scheinen unnötig zu sein, wenn Sie nur einen Durchgang über die Daten machen.
James M. Lay

@ JamesM.Lay: Wenn Sie jedes Element nur einmal berühren, kann eine bessere Durchquerungsreihenfolge eine räumliche Lokalität ergeben. (Verwenden Sie z. B. alle Bytes einer Cache-Zeile, die Sie berührt haben, anstatt die Spalten einer Matrix mit einem Element pro Cache-Zeile zu durchlaufen.)
Peter Cordes

42

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.


4
+1, obwohl der letzte Satz nicht wirklich in diese Diskussion gehört - man würde annehmen, dass der Assembler erst ins Spiel kommt, nachdem alle möglichen Verbesserungen des Algorithmus usw. realisiert wurden.
mghie

18
@Matt: Handgeschriebenes ASM ist auf einigen der winzigen CPUs, mit denen EE zusammenarbeitet und die beschissene Hersteller-Compiler-Unterstützung bieten, oft viel besser.
Zan Lynx

5
"Nur bei Verwendung spezieller Befehlssätze" ?? Sie haben wahrscheinlich noch nie einen handoptimierten ASM-Code geschrieben. Eine mäßig vertraute Kenntnis der Architektur, an der Sie arbeiten, bietet Ihnen eine gute Chance, einen besseren Code (Größe und Geschwindigkeit) als Ihr Compiler zu generieren. Wie @mghie kommentierte, beginnen Sie natürlich immer damit, die besten Algen zu codieren, die Sie für Ihr Problem verwenden können. Selbst für sehr gute Compiler müssen Sie Ihren C-Code so schreiben, dass der Compiler zum besten kompilierten Code gelangt. Andernfalls ist der generierte Code nicht optimal.
Ysap

2
@ysap - Auf tatsächlichen Computern (keine winzigen eingebetteten Chips mit geringer Leistung) in der realen Welt wird der "optimale" Code nicht schneller sein, da bei großen Datenmengen die Leistung durch Speicherzugriff und Seitenfehler eingeschränkt wird ( und wenn Sie keinen großen Datensatz haben, wird dies in beiden Fällen schnell gehen und es macht keinen Sinn, ihn zu optimieren) - heutzutage arbeite ich hauptsächlich in C # (nicht einmal in c) und die Leistungssteigerungen durch das Komprimieren des Speichermanagers werden Gewichtung des Overheads für die Speicherbereinigung, Komprimierung und JIT-Kompilierung.
Nir

4
+1 für die Angabe, dass Compiler (insbesondere JIT) bessere Arbeit leisten können als Menschen, wenn sie für die Hardware optimiert sind, auf der sie ausgeführt werden.
Sebastian

38

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:

  1. 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:

    int16_t x, y;
    // int16_t is a typedef for "short"
    // set x and y to something
    int16_t prod = (int16_t)(((int32_t)x*y)>>16);`

    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.

  2. Bestimmte Bitverschiebungsvorgänge (Drehung / Übertragen):

    // 256-bit array shifted right in its entirety:
    uint8_t x[32];
    for (int i = 32; --i > 0; )
    {
       x[i] = (x[i] >> 1) | (x[i-1] << 7);
    }
    x[0] >>= 1;

    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.


23

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):

Die Programmiersprache C - Eine Sprache, die die Flexibilität der Assemblersprache mit der Leistungsfähigkeit der Assemblersprache kombiniert.

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).


Es gab viele Leute, die ARM Assembler als Sprache der Wahl auf Acorn-Maschinen verwendeten (Anfang der 90er Jahre). IIRC sagten sie, dass der kleine Risc-Befehlssatz es einfacher und lustiger machte. Ich vermute jedoch, dass der C-Compiler für Acorn verspätet eingetroffen ist und der C ++ - Compiler nie fertiggestellt wurde.
Andrew M

3
"... weil es für C keinen subjektiven Standard gibt." Du meinst objektiv .
Thomas

@ AndrewM: Ja, ich habe ungefähr 10 Jahre lang Anwendungen in gemischten Sprachen in BASIC und ARM Assembler geschrieben. Ich habe C in dieser Zeit gelernt, aber es war nicht sehr nützlich, weil es so umständlich wie Assembler und langsamer ist. Norcroft hat einige großartige Optimierungen vorgenommen, aber ich denke, der bedingte Befehlssatz war ein Problem für die Compiler des Tages.
Jon Harrop

1
@ AndrewM: Nun, eigentlich ist ARM eine Art RISC, das rückwärts gemacht wird. Andere RISC-ISAs wurden entwickelt, beginnend mit dem, was ein Compiler verwenden würde. Der ARM ISA scheint so konzipiert worden zu sein, dass er mit dem beginnt, was die CPU bietet (Barrel Shifter, Bedingungsflags → Lassen Sie sie in jeder Anweisung verfügbar machen).
Ninjalj

16

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.


Der Amiga hatte keine 16 MB Chip-RAM, eher 512 kB bis 2 MB, abhängig vom Chipsatz. Außerdem funktionierten viele Amiga-Spiele aufgrund der von Ihnen beschriebenen Techniken nicht mit schnelleren CPUs.
bk1e

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

@ bk1e: Ich stehe korrigiert. Mein Speicher kann ausfallen, aber war der Chip-RAM nicht auf den ersten 24-Bit-Adressraum (dh 16 MB) beschränkt? Und Fast wurde darüber abgebildet?
Aaron Digulla

@ Aaron Digulla: Wikipedia hat mehr Informationen über die Unterscheidung zwischen Chip / schnell / langsam RAM: en.wikipedia.org/wiki/Amiga_Chip_RAM
bk1e

@ bk1e: Mein Fehler. Die 68k-CPU hatte nur 24 Adressspuren, deshalb hatte ich die 16MB im Kopf.
Aaron Digulla

15

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?

  1. Entfernen Sie eine vorhandene Funktion
  2. Reduzieren Sie die Größe einiger oder aller vorhandenen Funktionen möglicherweise auf Kosten der Leistung.
  3. Befürworten Sie den Wechsel zu einem größeren Chip mit höheren Kosten, höherem Stromverbrauch und größerem Formfaktor.

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

  • Arbeiten an der Betriebssystemfunktion auf niedriger Ebene
  • Arbeiten an einem Compiler.
  • Arbeiten an einem extrem begrenzten Chip, einem eingebetteten System usw.

Denken Sie daran, Ihre Assembly mit dem generierten Compiler zu vergleichen, um festzustellen, welche schneller / kleiner / besser ist.

David.


4
+1 für die Berücksichtigung eingebetteter Anwendungen auf winzigen Chips. Zu viele Softwareentwickler hier denken entweder nicht an Embedded oder denken, dass dies ein Smartphone bedeutet (32 Bit, MB RAM, MB Flash).
Martin

1
Time Embedded-Anwendungen sind ein gutes Beispiel! Es gibt oft seltsame Anweisungen (auch wirklich einfache wie avr's sbiund cbi), die Compiler aufgrund ihrer begrenzten Kenntnisse der Hardware früher (und manchmal immer noch) nicht voll ausnutzen.
Felixphew

15

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önnen

int c;
for(c = 0; str[c] != '\0'; c++) {}

Während der Montage können Sie dies erheblich beschleunigen:

mov esi, offset string
mov edi, esi
xor ecx, ecx

lp:
mov ax, byte ptr [esi]
cmp al, cl
je  end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp

end_4:
inc esi

end_3:
inc esi

end_2:
inc esi

end_1:
inc esi

mov ecx, esi
sub ecx, edi

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!



@ninjalj: sie sind das gleiche :) Ich dachte nicht, dass es so in C gemacht werden kann. Es kann leicht verbessert werden, denke ich
BlackBear

Vor jedem Vergleich im C-Code gibt es noch eine bitweise UND-Verknüpfung. Es ist möglich, dass der Compiler klug genug ist, um dies auf Vergleiche mit hohen und niedrigen Bytes zu reduzieren, aber ich würde kein Geld darauf setzen. Es gibt tatsächlich einen schnelleren Schleifenalgorithmus, der auf der Eigenschaft (word & 0xFEFEFEFF) & (~word + 0x80808080)Null basiert, wenn alle Bytes im Wort ungleich Null sind.
user2310967

@MichaWiedenmann stimmt, ich sollte bx laden, nachdem ich die beiden Zeichen in ax verglichen habe. Vielen Dank
BlackBear

14

Matrixoperationen mit SIMD-Anweisungen sind wahrscheinlich schneller als vom Compiler generierter Code.


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

Compiler erstellen SSE-fähigen Code, so dass dieses Argument nicht wahr ist
vartec

5
In vielen dieser Situationen können Sie SSE Intrics anstelle von Assembly verwenden. Dadurch wird Ihr Code portabler (gcc visual c ++, 64bit, 32bit usw.) und Sie müssen keine Registerzuweisung vornehmen.
Laserallan

1
Sicher würden Sie, aber die Frage fragte nicht, wo ich Assembly anstelle von C verwenden sollte. Es hieß, wenn der C-Compiler keinen besseren Code generiert. Ich habe eine C-Quelle angenommen, die keine direkten SSE-Aufrufe oder Inline-Assemblys verwendet.
Mehrdad Afshari

9
Mehrdad hat jedoch recht. SSE richtig zu machen ist für den Compiler ziemlich schwierig und selbst in offensichtlichen (für Menschen) Situationen, in denen die meisten Compiler es nicht einsetzen.
Konrad Rudolph

13

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.


8
+1: "Sie können vom Aufruf von Konventionen abweichen". C / C ++ - Compiler neigen dazu, bei der Rückgabe mehrerer Werte zu saugen. Sie verwenden häufig das sret-Formular, bei dem der Aufruferstapel einen zusammenhängenden Block für eine Struktur zuweist und einen Verweis darauf übergibt, damit der Angerufene ihn ausfüllen kann. Die Rückgabe mehrerer Werte in Registern ist um ein Vielfaches schneller.
Jon Harrop

1
@ Jon: C / C ++ - Compiler machen das ganz gut, wenn die Funktion inline wird (nicht inline Funktionen müssen dem ABI entsprechen, dies ist keine Einschränkung von C und C ++, sondern das Verknüpfungsmodell)
Ben Voigt

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

2
Ich sehe dort keinen Funktionsaufruf, der eingefügt wird.
Ben Voigt

13

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.


2
@Dennis, weshalb ich geschrieben habe: "Natürlich können Sie Intrinsics anstelle von Assembly verwenden, aber sobald Sie sich auf der Intrinsics-Ebene befinden, schreiben Sie im Grunde ohnehin Assembly, indem Sie den Compiler nur als Registerzuweiser und (nominell) Anweisungsplaner verwenden."
Jack Lloyd

Außerdem ist intrinsischer SIMD-Code in der Regel weniger lesbar als der gleiche Code, der in Assembler geschrieben wurde: Ein Großteil des SIMD-Codes beruht auf impliziten Neuinterpretationen der Daten in den Vektoren, was eine PITA im Zusammenhang mit den vom Compiler bereitgestellten Datentypen ist.
cmaster

10

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.


3
Die profilgesteuerte Optimierung gibt dem Compiler Informationen darüber, wie oft eine Schleife verwendet wird.
Zan Lynx

10

Ö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.


Qualitätscompiler für kleine Mikros können seit Jahren vermeiden, die oberen Teile von Werten zu verarbeiten, wenn dies die Ergebnisse niemals wesentlich beeinflussen könnte. Heraufstufungsregeln verursachen zwar Probleme, aber meistens in Fällen, in denen ein Compiler nicht wissen kann, welche Eckfälle relevant sind und welche nicht.
Supercat

9

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.


2
"Es ist also nicht notwendig, in Assembler zu schreiben, um den schnellsten Code aller Zeiten zu erhalten." Nun, ich habe keinen Compiler gesehen, der auf jeden Fall das Optimale getan hat, was nicht trivial war. Ein erfahrener Mensch kann in praktisch allen Fällen besser als der Compiler. Es ist also absolut notwendig, in Assembler zu schreiben, um "den schnellsten Code aller Zeiten" zu erhalten.
cmaster - wieder Monica

@cmaster Nach meiner Erfahrung ist die Compiler-Ausgabe gut, zufällig. Manchmal ist es wirklich gut und optimal und manchmal ist es "wie könnte dieser Müll ausgestoßen worden sein".
Scharfzahn

9

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, 1stattdessen 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.


8

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.


7

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.


6

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.




5

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.


1
Sie haben vergessen zu sagen, "mit viel Zeit und Mühe" und "einen Wartungsalptraum zu schaffen". Ein Kollege von mir arbeitete an der Optimierung eines leistungskritischen Abschnitts des Betriebssystemcodes, und er arbeitete viel mehr in C als in Assembly, da er die Auswirkungen von Änderungen auf hoher Ebene innerhalb eines angemessenen Zeitrahmens auf die Leistung untersuchen konnte.
Artelius

Genau. Manchmal verwenden Sie Makros und Skripte, um Assemblycode zu generieren, um Zeit zu sparen und sich schnell zu entwickeln. Die meisten Assembler haben heutzutage Makros; Wenn nicht, können Sie einen (einfachen) Makro-Vorprozessor mit einem (ziemlich einfachen RegEx) Perl-Skript erstellen.

Diese. Genau. Der Compiler gegen die Domain-Experten wurde noch nicht erfunden.
cmaster - wieder Monica

4

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).


4

Einer der bekanntesten Assemblierungsausschnitte stammt aus Michael Abrashs Textur-Mapping-Schleife ( hier ausführlich erläutert ):

add edx,[DeltaVFrac] ; add in dVFrac
sbb ebp,ebp ; store carry
mov [edi],al ; write pixel n
mov al,[esi] ; fetch pixel n+1
add ecx,ebx ; add in dUFrac
adc esi,[4*ebp + UVStepVCarry]; add in steps

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.


Noch besser ist, dass diese SSE-Eigenschaften von Intel spezifiziert werden, sodass sie eigentlich ziemlich portabel sind.
James

4

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.


Dies wäre etwas korrekter: "Es wäre schwierig, ein nichttriviales C-Programm zu erstellen , in dem ..." Alternativ könnte man sagen: "Es wäre schwierig, ein reales C-Programm zu finden, in dem ..." Punkt ist Es gibt triviale Schleifen, für die Compiler eine optimale Ausgabe erzeugen. Trotzdem gute Antwort.
cmaster


4

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.


8
GCC führt äußerst intelligente "plattformunabhängige" Optimierungen durch. Es ist jedoch nicht so gut, bestimmte Befehlssätze in vollem Umfang zu nutzen. Für einen solchen tragbaren Compiler macht es einen sehr guten Job.
Artelius

2
einverstanden. Die Portabilität, die eingehenden Sprachen und die ausgehenden Ziele sind erstaunlich. So tragbar zu sein, kann und kann es verhindern, dass man in einer Sprache oder einem Ziel wirklich gut ist. Die Möglichkeiten für einen Menschen, es besser zu machen, bestehen also für eine bestimmte Optimierung eines bestimmten Ziels.
old_timer

+1: GCC ist sicherlich nicht wettbewerbsfähig bei der Generierung von schnellem Code, aber ich bin mir nicht sicher, ob dies daran liegt, dass es portabel ist. LLVM ist portabel und ich habe gesehen, dass es Code 4x schneller generiert als GCCs.
Jon Harrop

Ich bevorzuge GCC, da es seit vielen Jahren absolut stabil ist und für fast jede Plattform verfügbar ist, auf der ein moderner tragbarer Compiler ausgeführt werden kann. Leider konnte ich LLVM (Mac OS X / PPC) nicht erstellen, sodass ich wahrscheinlich nicht darauf umsteigen kann. Eines der guten Dinge an GCC ist, dass Sie, wenn Sie Code schreiben, der in GCC erstellt wird, höchstwahrscheinlich die Standards einhalten und sicher sein können, dass er für fast jede Plattform erstellt werden kann.

4

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:

  • Gedächtnis ist langsam
  • Cache ist schnell
  • versuche besser zwischengespeichert zu verwenden
  • Wie oft wirst du vermissen? Haben Sie eine Latenzkompensationsstrategie?
  • Sie können 10-100 ALU / FPU / SSE-Anweisungen für einen einzelnen Cache-Fehler ausführen
  • Anwendungsarchitektur ist wichtig ..
  • .. aber es hilft nicht, wenn das Problem nicht in der Architektur liegt

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.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.