Ist die Inline-Assemblersprache langsamer als nativer C ++ - Code?

182

Ich habe versucht, die Leistung von Inline-Assemblersprache und C ++ - Code zu vergleichen, also habe ich eine Funktion geschrieben, die zwei Arrays der Größe 2000 100000 Mal hinzufügt. Hier ist der Code:

#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
    for(int i = 0; i < TIMES; i++)
    {
        for(int j = 0; j < length; j++)
            x[j] += y[j];
    }
}


void calcuAsm(int *x,int *y,int lengthOfArray)
{
    __asm
    {
        mov edi,TIMES
        start:
        mov esi,0
        mov ecx,lengthOfArray
        label:
        mov edx,x
        push edx
        mov eax,DWORD PTR [edx + esi*4]
        mov edx,y
        mov ebx,DWORD PTR [edx + esi*4]
        add eax,ebx
        pop edx
        mov [edx + esi*4],eax
        inc esi
        loop label
        dec edi
        cmp edi,0
        jnz start
    };
}

Hier ist main():

int main() {
    bool errorOccured = false;
    setbuf(stdout,NULL);
    int *xC,*xAsm,*yC,*yAsm;
    xC = new int[2000];
    xAsm = new int[2000];
    yC = new int[2000];
    yAsm = new int[2000];
    for(int i = 0; i < 2000; i++)
    {
        xC[i] = 0;
        xAsm[i] = 0;
        yC[i] = i;
        yAsm[i] = i;
    }
    time_t start = clock();
    calcuC(xC,yC,2000);

    //    calcuAsm(xAsm,yAsm,2000);
    //    for(int i = 0; i < 2000; i++)
    //    {
    //        if(xC[i] != xAsm[i])
    //        {
    //            cout<<"xC["<<i<<"]="<<xC[i]<<" "<<"xAsm["<<i<<"]="<<xAsm[i]<<endl;
    //            errorOccured = true;
    //            break;
    //        }
    //    }
    //    if(errorOccured)
    //        cout<<"Error occurs!"<<endl;
    //    else
    //        cout<<"Works fine!"<<endl;

    time_t end = clock();

    //    cout<<"time = "<<(float)(end - start) / CLOCKS_PER_SEC<<"\n";

    cout<<"time = "<<end - start<<endl;
    return 0;
}

Dann führe ich das Programm fünfmal aus, um die Zyklen des Prozessors zu erhalten, die als Zeit angesehen werden könnten. Jedes Mal rufe ich nur eine der oben genannten Funktionen auf.

Und hier kommt das Ergebnis.

Funktion der Baugruppenversion:

Debug   Release
---------------
732        668
733        680
659        672
667        675
684        694
Average:   677

Funktion der C ++ - Version:

Debug     Release
-----------------
1068      168
 999      166
1072      231
1002      166
1114      183
Average:  182

Der C ++ - Code im Release-Modus ist fast 3,7-mal schneller als der Assembly-Code. Warum?

Ich denke, dass der Assembler-Code, den ich geschrieben habe, nicht so effektiv ist wie der von GCC generierte. Für einen gewöhnlichen Programmierer wie mich ist es schwierig, Code schneller zu schreiben als sein Gegner, der von einem Compiler generiert wurde. Bedeutet das, dass ich der Leistung der von meinen Händen geschriebenen Assemblersprache nicht vertrauen, mich auf C ++ konzentrieren und die Assemblersprache vergessen sollte?

user957121
quelle
29
Ja schon. Handcodierte Assemblierung ist unter bestimmten Umständen angemessen, es muss jedoch darauf geachtet werden, dass die Assemblyversion tatsächlich schneller ist als mit einer höheren Sprache.
Magnus Hoff
161
Es kann hilfreich sein, den vom Compiler generierten Code zu studieren und zu verstehen, warum er schneller als Ihre Assembly-Version ist.
Paul R
34
Ja, es sieht so aus, als ob der Compiler besser als Sie schreiben kann. Moderne Compiler sind wirklich ziemlich gut.
David Heffernan
20
Haben Sie sich die von GCC produzierte Baugruppe angesehen? Sein möglicher GCC verwendete MMX-Anweisungen. Ihre Funktion ist sehr parallel - Sie könnten möglicherweise N Prozessoren verwenden, um die Summe in 1 / N der Zeit zu berechnen. Versuchen Sie eine Funktion, bei der keine Hoffnung auf Parallelisierung besteht.
Chris
11
Hm, ich hätte erwartet, dass ein guter Compiler dies ~ 100000-mal schneller macht ...
PlasmaHH

Antworten:

260

Ja, meistens.

Zunächst gehen Sie von der falschen Annahme aus, dass eine einfache Sprache (in diesem Fall Assembly) immer schnelleren Code erzeugt als eine höhere Sprache (in diesem Fall C ++ und C). Es ist nicht wahr. Ist C-Code immer schneller als Java-Code? Nein, weil es eine andere Variable gibt: Programmierer. Die Art und Weise, wie Sie Code schreiben, und die Kenntnis der Architekturdetails haben großen Einfluss auf die Leistung (wie Sie in diesem Fall gesehen haben).

Sie können immer ein Beispiel erstellen, in dem handgefertigter Assembler-Code besser ist als kompilierter Code, aber normalerweise ist es ein fiktives Beispiel oder eine einzelne Routine (kein echtes Programm mit mehr als 500.000 Zeilen C ++ - Code). Ich denke, Compiler produzieren 95% -igen besseren Assembler-Code, und manchmal, nur in seltenen Fällen, müssen Sie Assembler-Code für wenige, kurze, häufig verwendete , leistungskritische Routinen schreiben oder wenn Sie auf Funktionen Ihrer bevorzugten Hochsprache zugreifen müssen nicht aussetzen. Möchten Sie einen Hauch dieser Komplexität? Lesen Sie diese großartige Antwort hier auf SO.

Warum das?

Erstens, weil Compiler Optimierungen vornehmen können, die wir uns nicht einmal vorstellen können (siehe diese kurze Liste ), und sie werden sie in Sekunden ausführen (wenn wir Tage brauchen ).

Wenn Sie in Assembly codieren, müssen Sie genau definierte Funktionen mit einer genau definierten Aufrufschnittstelle erstellen. Sie können jedoch die Optimierung des gesamten Programms und die Optimierung zwischen den Prozeduren berücksichtigen, wie z. B. Registerzuweisung , konstante Ausbreitung , Eliminierung gemeinsamer Unterausdrücke , Befehlsplanung und andere komplexe, nicht offensichtliche Optimierungen ( z. B. Polytopemodell ). In Bezug auf die RISC- Architektur haben sich die Leute vor vielen Jahren keine Gedanken mehr darüber gemacht (die Befehlsplanung ist beispielsweise von Hand sehr schwer abzustimmen ), und moderne CISC- CPUs haben sehr lange Pipelines auch.

Bei einigen komplexen Mikrocontrollern werden sogar Systembibliotheken in C anstatt in Assembly geschrieben, da ihre Compiler einen besseren (und einfach zu wartenden) endgültigen Code erzeugen.

Compiler können manchmal einige MMX / SIMDx-Anweisungen automatisch selbst verwenden, und wenn Sie sie nicht verwenden, können Sie sie einfach nicht vergleichen (andere Antworten haben Ihren Assembler-Code bereits sehr gut überprüft). Nur für Schleifen ist dies eine kurze Liste von Schleifenoptimierungen dessen, was üblicherweise von einem Compiler überprüft wird (glauben Sie, Sie könnten es selbst tun, wenn Ihr Zeitplan für ein C # -Programm festgelegt wurde?) Wenn Sie etwas in Assembly schreiben, ich Ich denke, Sie müssen zumindest einige einfache Optimierungen berücksichtigen . Das Schulbuchbeispiel für Arrays ist das Abrollen des Zyklus (seine Größe ist zur Kompilierungszeit bekannt). Mach es und führe deinen Test erneut aus.

Heutzutage ist es auch sehr ungewöhnlich, Assemblersprache aus einem anderen Grund zu verwenden: der Fülle verschiedener CPUs . Willst du sie alle unterstützen? Jedes hat eine spezifische Mikroarchitektur und einige spezifische Befehlssätze . Sie haben eine unterschiedliche Anzahl von Funktionseinheiten und Montageanweisungen sollten so angeordnet werden, dass sie alle beschäftigt sind . Wenn Sie in C schreiben, können Sie PGO verwenden, aber in der Assembly benötigen Sie ein umfassendes Wissen über diese spezifische Architektur (und überdenken und wiederholen Sie alles für eine andere Architektur ). Bei kleinen Aufgaben erledigt der Compiler dies normalerweise besser, und bei komplexen Aufgaben wird die Arbeit normalerweise nicht zurückgezahlt (undCompiler kann es sowieso besser machen).

Wenn Sie sich hinsetzen und sich Ihren Code ansehen, werden Sie wahrscheinlich feststellen, dass Sie mehr für die Neugestaltung Ihres Algorithmus als für die Übersetzung in Assembly gewinnen (lesen Sie diesen großartigen Beitrag hier auf SO ). Es gibt Optimierungen auf hoher Ebene (und Hinweise für den Compiler) können Sie effektiv anwenden, bevor Sie auf die Assemblersprache zurückgreifen müssen. Es ist wahrscheinlich erwähnenswert, dass Sie bei Verwendung von Intrinsics häufig einen Leistungsgewinn erzielen, den Sie suchen, und der Compiler die meisten seiner Optimierungen weiterhin ausführen kann.

Selbst wenn Sie einen 5- bis 10-mal schnelleren Assembler-Code erstellen können, sollten Sie Ihre Kunden fragen, ob sie lieber eine Woche Ihrer Zeit bezahlen oder eine 50-Dollar-schnellere CPU kaufen möchten . Meistens ist eine extreme Optimierung (und insbesondere in LOB-Anwendungen) für die meisten von uns einfach nicht erforderlich.

Adriano Repetti
quelle
9
Natürlich nicht. Ich denke, es ist in 99% der Fälle besser für 95% der Menschen. Manchmal, weil es einfach zu teuer (wegen komplexer Mathematik) oder Zeitaufwand (dann wieder teuer) ist. Manchmal, weil wir Optimierungen einfach vergessen haben ...
Adriano Repetti
62
@ ja72 - nein, es ist nicht besser , Code zu schreiben . Es ist besser, Code zu optimieren .
Mike Baranczak
14
Es ist kontraintuitiv, bis Sie es wirklich in Betracht ziehen. Auf die gleiche Weise beginnen VM-basierte Maschinen, Laufzeitoptimierungen vorzunehmen, für die Compiler einfach nicht die erforderlichen Informationen haben.
Bill K
6
@ M28: Compiler können dieselben Anweisungen verwenden. Sicher, sie zahlen dafür in Form der Binärgröße (weil sie einen Fallback-Pfad angeben müssen, falls diese Anweisungen nicht unterstützt werden). Außerdem sind die "neuen Anweisungen", die hinzugefügt werden, größtenteils ohnehin SMID-Anweisungen, deren Verwendung sowohl von VMs als auch von Compilern ziemlich schrecklich ist. VMs zahlen für diese Funktion, indem sie den Code beim Start kompilieren müssen.
Billy ONeal
9
@ BillK: PGO macht dasselbe für Compiler.
Billy ONeal
194

Ihr Assembler-Code ist nicht optimal und kann verbessert werden:

  • Sie drücken und knallen ein Register ( EDX ) in Ihre innere Schleife. Dies sollte aus der Schleife verschoben werden.
  • Sie laden die Array-Zeiger in jeder Iteration der Schleife neu. Dies sollte aus der Schleife verschoben werden.
  • Sie verwenden die loopAnweisung, von der bekannt ist, dass sie auf den meisten modernen CPUs sehr langsam ist (möglicherweise aufgrund der Verwendung eines alten Montagebuchs *).
  • Sie nutzen das manuelle Abrollen der Schleife nicht aus.
  • Sie verwenden keine verfügbaren SIMD- Anweisungen.

Wenn Sie also Ihre Fähigkeiten in Bezug auf Assembler nicht wesentlich verbessern, ist es für Sie nicht sinnvoll, Assembler-Code für die Leistung zu schreiben.

* Natürlich weiß ich nicht, ob Sie wirklich die loopAnweisung aus einem alten Versammlungsbuch erhalten haben. Aber Sie sehen es fast nie im Code der realen Welt, da jeder Compiler da draußen klug genug ist, um nicht zu emittieren loop. Sie sehen es nur in IMHO schlechten und veralteten Büchern.

Gunther Piez
quelle
Compiler können immer noch loop(und viele "veraltete" Anweisungen) ausgeben, wenn Sie für die Größe optimieren
phuclv
1
@phuclv gut ja, aber die ursprüngliche Frage war genau über die Geschwindigkeit, nicht die Größe.
IGR94
60

Noch bevor wir uns mit Assembly befassen, gibt es Code-Transformationen, die auf einer höheren Ebene existieren.

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
  for (int i = 0; i < TIMES; i++) {
    for (int j = 0; j < length; j++) {
      x[j] += y[j];
    }
  }
}

kann über Loop Rotation umgewandelt werden :

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      for (int i = 0; i < TIMES; ++i) {
        x[j] += y[j];
      }
    }
}

Das ist viel besser, was die Speicherlokalität betrifft.

Dies könnte weiter optimiert werden. a += bX-mal zu machen ist gleichbedeutend a += X * bdamit:

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      x[j] += TIMES * y[j];
    }
}

Es scheint jedoch, dass mein Lieblingsoptimierer (LLVM) diese Transformation nicht durchführt.

[Bearbeiten] Ich habe festgestellt, dass die Transformation durchgeführt wird, wenn wir das restrictQualifikationsmerkmal für xund hatten y. In der Tat ohne diese Einschränkung, x[j]und y[j]könnte Alias ​​auf den gleichen Ort, der diese Transformation fehlerhaft macht. [Bearbeitung beenden]

Jedenfalls ist dies meiner Meinung nach die optimierte C-Version. Schon ist es viel einfacher. Basierend darauf ist hier mein Riss bei ASM (ich lasse Clang ihn generieren, ich bin nutzlos darin):

calcuAsm:                               # @calcuAsm
.Ltmp0:
    .cfi_startproc
# BB#0:
    testl   %edx, %edx
    jle .LBB0_2
    .align  16, 0x90
.LBB0_1:                                # %.lr.ph
                                        # =>This Inner Loop Header: Depth=1
    imull   $100000, (%rsi), %eax   # imm = 0x186A0
    addl    %eax, (%rdi)
    addq    $4, %rsi
    addq    $4, %rdi
    decl    %edx
    jne .LBB0_1
.LBB0_2:                                # %._crit_edge
    ret
.Ltmp1:
    .size   calcuAsm, .Ltmp1-calcuAsm
.Ltmp2:
    .cfi_endproc

Ich fürchte, ich verstehe nicht, woher all diese Anweisungen kommen, aber Sie können immer Spaß haben und versuchen, zu sehen, wie sie verglichen werden ... aber ich würde immer noch die optimierte C-Version anstelle der Assembly-Version im Code verwenden. viel tragbarer.

Matthieu M.
quelle
Vielen Dank für Ihre Antwort. Nun, es ist ein wenig verwirrend, dass ich bei der Teilnahme an der Klasse "Compiler-Prinzipien" erfahren habe, dass der Compiler unseren Code auf viele Arten optimieren wird. Bedeutet das, dass wir unseren Code manuell optimieren müssen? Können wir einen besseren Job machen als der Compiler? Das ist die Frage, die mich immer verwirrt.
user957121
2
@ user957121: Wir können es besser optimieren, wenn wir mehr Informationen haben. Speziell hier behindert der Compiler das mögliche Aliasing zwischen xund y. Das heißt, der Compiler kann nicht sicher sein, dass für alle i,jin [0, length)uns haben x + i != y + j. Bei Überlappungen ist eine Optimierung nicht möglich. Die C-Sprache führte das restrictSchlüsselwort ein, um dem Compiler mitzuteilen, dass zwei Zeiger keinen Alias ​​haben können. Dies funktioniert jedoch nicht für Arrays, da sie sich auch dann überlappen können, wenn sie nicht genau Alias ​​sind.
Matthieu M.
Aktuelle GCC- und Clang-Vektorisierung (nach Überprüfung auf Nichtüberlappung, wenn Sie dies weglassen __restrict). SSE2 ist die Basis für x86-64 und mit dem Mischen kann SSE2 2x 32-Bit-Multiplikationen gleichzeitig ausführen (64-Bit-Produkte werden erzeugt, daher das Mischen, um die Ergebnisse wieder zusammenzusetzen). godbolt.org/z/r7F_uo . (SSE4.1 wird benötigt für pmulld: gepackte 32x32 => 32-Bit-Multiplikation). GCC hat einen tollen Trick, konstante ganzzahlige Multiplikatoren in Shift / Add (und / oder Subtrahieren) umzuwandeln, was für Multiplikatoren mit wenigen gesetzten Bits gut ist. Clangs Shuffle-lastiger Code wird einen Engpass beim Shuffle-Durchsatz auf Intel-CPUs verursachen.
Peter Cordes
41

Kurze Antwort: ja.

Lange Antwort: Ja, es sei denn, Sie wissen wirklich, was Sie tun, und haben einen Grund dafür.

Oliver Charlesworth
quelle
3
und dann nur, wenn Sie ein Profiling-Tool auf Assembly-Ebene wie vtune für Intel-Chips ausgeführt haben, um zu sehen, wo Sie möglicherweise Verbesserungen
Mark Mullin,
1
Dies beantwortet technisch die Frage, ist aber auch völlig nutzlos. A -1 von mir.
Navin
2
Sehr lange Antwort: "Ja, es sei denn, Sie möchten Ihren gesamten Code ändern, wenn eine neue (er) CPU verwendet wird. Wählen Sie den besten Algorithmus aus, aber lassen Sie den Compiler die Optimierung durchführen"
Tommylee2k
35

Ich habe meinen ASM-Code korrigiert:

  __asm
{   
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,1
    mov edi,y
label:
    movq mm0,QWORD PTR[esi]
    paddd mm0,QWORD PTR[edi]
    add edi,8
    movq QWORD PTR[esi],mm0
    add esi,8
    dec ecx 
    jnz label
    dec ebx
    jnz start
};

Ergebnisse für Release-Version:

 Function of assembly version: 81
 Function of C++ version: 161

Der Assemblycode im Release-Modus ist fast zweimal schneller als in C ++.

Sasha
quelle
18
Wenn Sie jetzt SSE anstelle von MMX verwenden (Registername ist xmm0anstelle von mm0), erhalten Sie eine weitere Beschleunigung um den Faktor zwei ;-)
Gunther Piez
8
Ich habe mich geändert und 41 für die Baugruppenversion bekommen. Es ist in 4 mal schneller :)
Sasha
3
kann auch bis zu 5% mehr bekommen, wenn alle xmm-Register verwendet werden
sasha
7
Wenn Sie jetzt über die Zeit nachdenken, die Sie tatsächlich gebraucht haben: Montage, ungefähr 10 Stunden oder so? C ++, ein paar Minuten, denke ich? Hier gibt es einen klaren Gewinner, es sei denn, es handelt sich um leistungskritischen Code.
Calimo
1
Ein guter Compiler wird bereits automatisch mit vektorisieren paddd xmm(nachdem er auf Überlappungen zwischen xund geprüft hat y, weil Sie diese nicht verwendet haben int *__restrict x). Zum Beispiel macht gcc das: godbolt.org/z/c2JG0- . Oder nach dem Inlining mainsollte es nicht auf Überlappung prüfen müssen, da es die Zuordnung sehen und beweisen kann, dass sie sich nicht überlappen. (Bei einigen x86-64-Implementierungen wird auch eine 16-Byte-Ausrichtung angenommen, was bei der eigenständigen Definition nicht der Fall ist.) Und wenn Sie mit kompilieren gcc -O3 -march=native, können Sie 256-Bit oder 512-Bit erhalten Vektorisierung.
Peter Cordes
24

Heißt das, ich sollte der Leistung der von meinen Händen geschriebenen Assemblersprache nicht vertrauen

Ja, genau das bedeutet es und es gilt für jede Sprache. Wenn Sie nicht wissen, wie man effizienten Code in Sprache X schreibt, sollten Sie Ihrer Fähigkeit, effizienten Code in X zu schreiben, nicht vertrauen. Wenn Sie also effizienten Code wünschen, sollten Sie eine andere Sprache verwenden.

Die Montage reagiert besonders empfindlich darauf, denn was Sie sehen, ist das, was Sie bekommen. Sie schreiben die spezifischen Anweisungen, die die CPU ausführen soll. Bei Hochsprachen gibt es zwischendurch einen Compiler, der Ihren Code transformieren und viele Ineffizienzen beseitigen kann. Mit der Montage sind Sie auf sich allein gestellt.

jalf
quelle
2
Ich denke, um zu schreiben, dass es besonders für einen modernen x86-Prozessor außerordentlich schwierig ist, effizienten Assembler-Code zu schreiben, da Pipelines, mehrere Ausführungseinheiten und andere Gimmicks in jedem Kern vorhanden sind. Das Schreiben von Code, der die Verwendung all dieser Ressourcen ausgleicht, um die höchste Ausführungsgeschwindigkeit zu erzielen, führt häufig zu Code mit einer unkomplizierten Logik, die nach "herkömmlicher" Assembler-Weisheit "nicht schnell" sein sollte. Bei weniger komplexen CPUs kann die Codegenerierung des C-Compilers meiner Erfahrung nach erheblich verbessert werden.
Olof Forshell
4
Der C-Compiler-Code kann normalerweise sogar auf einer modernen x86-CPU verbessert werden. Aber Sie müssen die CPU gut verstehen, was mit einer modernen x86-CPU schwieriger zu tun ist. Das ist mein Punkt. Wenn Sie die Hardware, auf die Sie abzielen, nicht verstehen, können Sie sie nicht optimieren. Und dann wird der Compiler wahrscheinlich einen besseren Job machen
Jalf
1
Und wenn Sie den Compiler wirklich umhauen wollen, müssen Sie kreativ sein und auf eine Weise optimieren, die der Compiler nicht kann. Es ist ein Kompromiss zwischen Zeit und Belohnung, deshalb ist C für einige eine Skriptsprache und für andere eine Zwischensprache für eine höhere Sprache. Für mich ist die Montage mehr zum Spaß :). ähnlich wie grc.com/smgassembly.htm
Hawken
22

Der einzige Grund, heutzutage Assemblersprache zu verwenden, besteht darin, einige Funktionen zu verwenden, auf die die Sprache nicht zugreifen kann.

Dies gilt für:

  • Kernel-Programmierung, die auf bestimmte Hardwarefunktionen wie die MMU zugreifen muss
  • Hochleistungsprogrammierung, die sehr spezifische Vektor- oder Multimedia-Anweisungen verwendet, die von Ihrem Compiler nicht unterstützt werden.

Aktuelle Compiler sind jedoch ziemlich schlau. Sie können sogar zwei separate Anweisungen wie d = a / b; r = a % b;durch eine einzige Anweisung ersetzen , die die Division und den Rest auf einmal berechnet, wenn sie verfügbar sind, selbst wenn C keinen solchen Operator hat.

fortran
quelle
10
Neben diesen beiden gibt es noch andere Orte für ASM. Eine Bignum-Bibliothek ist in ASM normalerweise wesentlich schneller als C, da sie Zugriff auf Übertragsflags und den oberen Teil der Multiplikation und dergleichen hat. Sie können diese Dinge auch in tragbarem C ausführen, aber sie sind sehr langsam.
Mooing Duck
@MooingDuck Dies kann als Zugriff auf Hardware-Hardwarefunktionen angesehen werden, die nicht direkt in der Sprache verfügbar sind. Solange Sie jedoch nur Ihren High-Level-Code von Hand in Assembly übersetzen , wird Sie der Compiler schlagen.
Fortan
1
es ist das, aber es ist weder Kernel-Programmierung noch herstellerspezifisch. Obwohl mit geringfügigen Änderungen in der Arbeitsweise, könnte es leicht in jede Kategorie fallen. Ich würde ASM erraten, wenn Sie die Leistung von Prozessoranweisungen ohne C-Zuordnung wünschen.
Mooing Duck
1
@fortran Du sagst im Grunde nur, wenn du deinen Code nicht optimierst, ist er nicht so schnell wie der Code, den der Compiler optimiert hat. Die Optimierung ist der Grund, warum man überhaupt eine Baugruppe schreiben würde. Wenn Sie übersetzen und dann optimieren meinen, gibt es keinen Grund, warum der Compiler Sie schlagen wird, es sei denn, Sie sind nicht gut darin, die Assembly zu optimieren. Um den Compiler zu schlagen, müssen Sie auf eine Weise optimieren, die der Compiler nicht kann. Es ist ziemlich selbsterklärend. Der einzige Grund, eine Assembly zu schreiben, besteht darin, dass Sie besser als ein Compiler / Interpreter sind . Das war schon immer der praktische Grund, eine Baugruppe zu schreiben.
Hawken
1
Nur zu sagen: Clang hat Zugriff auf die Übertragsflags, 128-Bit-Multiplikation und so weiter über integrierte Funktionen. Und all dies kann es in seine normalen Optimierungsalgorithmen integrieren.
Gnasher729
19

Es ist wahr, dass ein moderner Compiler bei der Codeoptimierung hervorragende Arbeit leistet, aber ich möchte Sie trotzdem ermutigen, weiterhin Assembler zu lernen.

Zunächst einmal lassen Sie sich davon eindeutig nicht einschüchtern , das ist ein großartiges Plus. Als Nächstes sind Sie auf dem richtigen Weg, indem Sie ein Profil erstellen, um Ihre Geschwindigkeitsannahmen zu validieren oder zu verwerfen . Sie bitten erfahrene Personen und Sie um Input haben das größte Optimierungswerkzeug, das der Menschheit bekannt ist: ein Gehirn .

Mit zunehmender Erfahrung erfahren Sie, wann und wo Sie es verwenden müssen (normalerweise die engsten, innersten Schleifen in Ihrem Code, nachdem Sie auf algorithmischer Ebene tief optimiert haben).

Als Inspiration würde ich Ihnen empfehlen , die Artikel von Michael Abrash nachzuschlagen (wenn Sie nichts von ihm gehört haben, ist er ein Optimierungsguru; er hat sogar mit John Carmack bei der Optimierung des Quake-Software-Renderers zusammengearbeitet!)

"Es gibt keinen schnellsten Code" - Michael Abrash


quelle
2
Ich glaube, eines der Bücher von Michael Abrash ist das Black Book zur Grafikprogrammierung. Aber er ist nicht der einzige, der Montage verwendet. Chris Sawyer hat die ersten beiden Achterbahn-Tycoon-Spiele in Montage selbst geschrieben.
Hawken
14

Ich habe den ASM-Code geändert:

 __asm
{ 
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,2
    mov edi,y
label:
    mov eax,DWORD PTR [esi]
    add eax,DWORD PTR [edi]
    add edi,4   
    dec ecx 
    mov DWORD PTR [esi],eax
    add esi,4
    test ecx,ecx
    jnz label
    dec ebx
    test ebx,ebx
    jnz start
};

Ergebnisse für Release-Version:

 Function of assembly version: 41
 Function of C++ version: 161

Der Assemblycode im Release-Modus ist fast viermal schneller als in C ++. IMHo hängt die Geschwindigkeit des Assembler-Codes vom Programmierer ab

Sasha
quelle
Ja, mein Code muss wirklich optimiert werden. Gute Arbeit für Sie und danke!
user957121
5
Es ist viermal schneller, weil Sie nur ein Viertel der Arbeit erledigen :-) Das shr ecx,2ist überflüssig, weil die Array-Länge bereits in intund nicht in Byte angegeben ist. Sie erreichen also im Grunde die gleiche Geschwindigkeit. Sie könnten die padddAntwort von Harold versuchen , dies wird wirklich schneller sein.
Gunther Piez
13

Es ist ein sehr interessantes Thema!
Ich habe das MMX von SSE in Sashas Code geändert.
Hier sind meine Ergebnisse:

Function of C++ version:      315
Function of assembly(simply): 312
Function of assembly  (MMX):  136
Function of assembly  (SSE):  62

Der Assemblycode mit SSE ist fünfmal schneller als mit C ++

Salaoshi
quelle
12

Die meisten Hochsprachen-Compiler sind sehr optimiert und wissen, was sie tun. Sie können versuchen, den Disassemble-Code zu sichern und ihn mit Ihrer nativen Assembly zu vergleichen. Ich glaube, Sie werden einige nette Tricks sehen, die Ihr Compiler verwendet.

Nur zum Beispiel, auch wenn ich nicht mehr sicher bin, ob es richtig ist :):

Tun:

mov eax,0

kosten mehr Zyklen als

xor eax,eax

das macht das gleiche.

Der Compiler kennt alle diese Tricks und verwendet sie.

Nuno_147
quelle
4
Immer noch wahr, siehe stackoverflow.com/questions/1396527/… . Nicht wegen der verwendeten Zyklen, sondern wegen des reduzierten Speicherbedarfs.
Gunther Piez
10

Der Compiler hat dich geschlagen. Ich werde es versuchen, aber ich werde keine Garantien geben. Ich gehe davon aus, dass die „Multiplikation“ von TIMES gemeint ist es ein relevanter Performance - Test zu machen, dass yund xbin 16 ausgerichtet, und das lengthist ein nicht-Null Vielfachen von 4. Das ist wahrscheinlich alles wahr sowieso.

  mov ecx,length
  lea esi,[y+4*ecx]
  lea edi,[x+4*ecx]
  neg ecx
loop:
  movdqa xmm0,[esi+4*ecx]
  paddd xmm0,[edi+4*ecx]
  movdqa [edi+4*ecx],xmm0
  add ecx,4
  jnz loop

Wie gesagt, ich gebe keine Garantie. Aber ich bin überrascht, wenn es viel schneller geht - der Engpass hier ist der Speicherdurchsatz, auch wenn alles ein L1-Treffer ist.

Harold
quelle
Ich denke, die komplexe Adressierung verlangsamt Ihren Code. Wenn Sie den Code in mov ecx, length, lea ecx,[ecx*4], mov eax,16... add ecx,eax[esi + ecx] ändern und dann überall verwenden, vermeiden Sie 1 Zyklusstillstand pro Befehl, der die Schleifenlose beschleunigt. (Wenn Sie den neuesten Skylake haben, gilt dies nicht). Das Hinzufügen von reg, reg macht die Schleife nur enger, was möglicherweise hilft oder nicht.
Johan
@Johan, das sollte kein Stall sein, nur eine zusätzliche Zykluslatenz, aber sicher kann es nicht schaden, es nicht zu haben. Ich habe diesen Code für Core2 geschrieben, der dieses Problem nicht hatte. Ist r + r nicht auch "komplex"?
Harold
7

Die blinde Implementierung des exakt gleichen Algorithmus Befehl für Befehl in der Assembly ist garantiert langsamer als das, was der Compiler tun kann.

Dies liegt daran, dass selbst die kleinste Optimierung, die der Compiler vornimmt, besser ist als Ihr starrer Code, ohne dass eine Optimierung erfolgt.

Natürlich ist es möglich, den Compiler zu schlagen, besonders wenn es sich um einen kleinen, lokalisierten Teil des Codes handelt. Ich musste es sogar selbst tun, um eine Ca. 4x beschleunigen, aber in diesem Fall müssen wir uns stark auf gute Kenntnisse der Hardware und zahlreiche, scheinbar kontraintuitive Tricks verlassen.

vsz
quelle
3
Ich denke, das hängt von der Sprache und dem Compiler ab. Ich kann mir einen äußerst ineffizienten C-Compiler vorstellen, dessen Ausgabe von einer unkomplizierten menschlichen Schreibanordnung leicht übertroffen werden könnte. Der GCC, nicht so sehr.
Casey Rodarmor
Da C / ++ - Compiler ein solches Unterfangen sind und es nur drei Hauptunternehmen gibt, sind sie in der Regel ziemlich gut darin, was sie tun. Unter bestimmten Umständen ist es immer noch (sehr) möglich, dass die handschriftliche Montage schneller erfolgt. Viele Mathematikbibliotheken werden auf asm verschoben, um mehrere / breite Werte besser verarbeiten zu können. Obwohl garantiert etwas zu stark ist, ist es wahrscheinlich.
ssube
@peachykeen: Ich habe nicht gemeint, dass die Assembly garantiert langsamer als C ++ im Allgemeinen ist. Ich meinte diese "Garantie" für den Fall, dass Sie einen C ++ - Code haben und ihn blind Zeile für Zeile in Assembly übersetzen. Lesen Sie auch den letzten Absatz meiner Antwort :)
vsz
5

Als Compiler würde ich eine Schleife mit einer festen Größe für viele Ausführungsaufgaben ersetzen.

int a = 10;
for (int i = 0; i < 3; i += 1) {
    a = a + i;
}

wird herstellen

int a = 10;
a = a + 0;
a = a + 1;
a = a + 2;

und irgendwann wird es wissen, dass "a = a + 0;" ist nutzlos, so dass diese Zeile entfernt wird. Hoffentlich etwas in Ihrem Kopf, das jetzt bereit ist, einige Optimierungsoptionen als Kommentar beizufügen. All diese sehr effektiven Optimierungen beschleunigen die kompilierte Sprache.

Miah
quelle
4
Und wenn aes nicht volatil ist, besteht eine gute Chance, dass der Compiler dies int a = 13;von Anfang an tut .
vsz
4

Genau das bedeutet es. Überlassen Sie die Mikrooptimierungen dem Compiler.

Luchian Grigore
quelle
4

Ich liebe dieses Beispiel, weil es eine wichtige Lektion über Low-Level-Code zeigt. Ja, Sie können Assemblys schreiben, die so schnell sind wie Ihr C-Code. Dies ist tautologisch wahr, aber nicht unbedingt Mittel nichts. Klar jemand kann, sonst wäre der Assembler nicht die entsprechenden Optimierungen kennen.

Ebenso gilt das gleiche Prinzip, wenn Sie die Hierarchie der Sprachabstraktion aufsteigen. Ja, Sie können einen Parser in C schreiben, der so schnell ist wie ein schnelles und schmutziges Perl-Skript, und das tun viele Leute. Das heißt aber nicht, dass Ihr Code schnell ist, weil Sie C verwendet haben. In vielen Fällen führen die übergeordneten Sprachen Optimierungen durch, die Sie möglicherweise noch nie in Betracht gezogen haben.

tylerl
quelle
3

In vielen Fällen kann die optimale Ausführung einer Aufgabe vom Kontext abhängen, in dem die Aufgabe ausgeführt wird. Wenn eine Routine in Assemblersprache geschrieben ist, ist es im Allgemeinen nicht möglich, die Reihenfolge der Anweisungen je nach Kontext zu variieren. Betrachten Sie als einfaches Beispiel die folgende einfache Methode:

inline void set_port_high(void)
{
  (*((volatile unsigned char*)0x40001204) = 0xFF);
}

Ein Compiler für 32-Bit-ARM-Code würde ihn angesichts der obigen Ausführungen wahrscheinlich wie folgt rendern:

ldr  r0,=0x40001204
mov  r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]

oder vielleicht

ldr  r0,=0x40001000  ; Some assemblers like to round pointer loads to multiples of 4096
mov  r1,#0
strb r1,[r0+0x204]
[a fourth word somewhere holding the constant 0x40001000]

Das könnte im handmontierten Code leicht optimiert werden, wie entweder:

ldr  r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]

oder

mvn  r0,#0xC0       ; Load with 0x3FFFFFFF
add  r0,r0,#0x1200  ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]

Beide von Hand zusammengestellten Ansätze würden 12 Bytes Code-Speicherplatz anstelle von 16 erfordern; Letzteres würde ein "Laden" durch ein "Hinzufügen" ersetzen, das auf einem ARM7-TDMI zwei Zyklen schneller ausführen würde. Wenn der Code in einem Kontext ausgeführt werden würde, in dem r0 nicht bekannt / egal ist, wären die Assembler-Versionen daher etwas besser als die kompilierte Version. Angenommen, der Compiler wusste, dass ein Register [z. B. r5] einen Wert enthalten würde, der innerhalb von 2047 Bytes der gewünschten Adresse 0x40001204 [z. B. 0x40001000] liegt, und wusste weiter, dass ein anderes Register [z. B. r7] ausgeführt wird um einen Wert zu halten, dessen niedrige Bits 0xFF waren. In diesem Fall könnte ein Compiler die C-Version des Codes optimieren, um einfach:

strb r7,[r5+0x204]

Viel kürzer und schneller als selbst der handoptimierte Assembler-Code. Angenommen, set_port_high ist im Kontext aufgetreten:

int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this

Überhaupt nicht unplausibel beim Codieren für ein eingebettetes System. Wenn set_port_highes in Assembly-Code geschrieben ist, müsste der Compiler r0 (das den Rückgabewert von enthält function1) an eine andere Stelle verschieben, bevor er den Assembly-Code aufruft, und diesen Wert anschließend wieder auf r0 verschieben (dafunction2 der erste Parameter in r0 erwartet wird). Der "optimierte" Assembler-Code würde also fünf Anweisungen benötigen. Selbst wenn der Compiler keine Register kennen würde, die die Adresse oder den zu speichernden Wert enthalten, würde seine Version mit vier Befehlen (die er anpassen könnte, um alle verfügbaren Register zu verwenden - nicht unbedingt r0 und r1) die "optimierte" Assembly schlagen -sprachige Version. Wenn der Compiler die erforderlichen Adressen und Daten in r5 und r7 hätte, wie zuvor beschrieben, function1würde er diese Register nicht ändern und könnte sie somit ersetzenset_port_highmitstrb Anweisung -vier Anweisungen kleiner und schneller als der "handoptimierte" Assembler-Code.

Beachten Sie, dass handoptimierter Assembler-Code einen Compiler häufig übertreffen kann, wenn der Programmierer den genauen Programmablauf kennt, Compiler jedoch in Fällen glänzen, in denen ein Teil des Codes geschrieben wurde, bevor sein Kontext bekannt ist, oder wenn ein Teil des Quellcodes vorhanden ist aus mehreren Kontexten aufgerufen [if set_port_high der Compiler an fünfzig verschiedenen Stellen im Code verwendet wird, kann er unabhängig für jeden von ihnen entscheiden, wie er am besten erweitert werden soll].

Im Allgemeinen würde ich vorschlagen, dass die Assemblersprache in den Fällen, in denen jeder Code aus einer sehr begrenzten Anzahl von Kontexten heraus aufgerufen werden kann, die größten Leistungsverbesserungen erzielt und die Leistung an Orten beeinträchtigt, an denen ein Teil von Code vorhanden ist Code kann aus vielen verschiedenen Kontexten betrachtet werden. Interessanterweise (und bequemerweise) sind die Fälle, in denen die Montage für die Leistung am vorteilhaftesten ist, häufig diejenigen, in denen der Code am einfachsten und am einfachsten zu lesen ist. Die Stellen, an denen Assembler-Code zu einem Durcheinander wird, sind häufig diejenigen, an denen das Schreiben in Assembler den geringsten Leistungsvorteil bietet.

[Kleiner Hinweis: Es gibt einige Stellen, an denen Assembler-Code verwendet werden kann, um ein hyperoptimiertes, klebriges Durcheinander zu erzielen. Zum Beispiel musste ein Code, den ich für den ARM gemacht habe, ein Wort aus dem RAM abrufen und eine von ungefähr zwölf Routinen basierend auf den oberen sechs Bits des Werts ausführen (viele Werte sind derselben Routine zugeordnet). Ich glaube, ich habe diesen Code so optimiert:

ldrh  r0,[r1],#2! ; Fetch with post-increment
ldrb  r1,[r8,r0 asr #10]
sub   pc,r8,r1,asl #2

Das Register r8 enthielt immer die Adresse der Hauptversandtabelle (innerhalb der Schleife, in der der Code 98% seiner Zeit verbringt, wurde er nie für einen anderen Zweck verwendet); Alle 64 Einträge beziehen sich auf Adressen in den 256 Bytes davor. Da die primäre Schleife in den meisten Fällen ein hartes Ausführungszeitlimit von etwa 60 Zyklen hatte, war das Abrufen und Versenden von neun Zyklen sehr hilfreich, um dieses Ziel zu erreichen. Die Verwendung einer Tabelle mit 256 32-Bit-Adressen wäre einen Zyklus schneller gewesen, hätte jedoch 1 KB sehr wertvollen Arbeitsspeicher verschlungen [Flash hätte mehr als einen Wartezustand hinzugefügt]. Die Verwendung von 64 32-Bit-Adressen hätte das Hinzufügen eines Befehls zum Maskieren einiger Bits aus dem abgerufenen Wort erforderlich gemacht und immer noch 192 Bytes mehr verschlungen als die Tabelle, die ich tatsächlich verwendet habe. Die Verwendung der Tabelle der 8-Bit-Offsets ergab einen sehr kompakten und schnellen Code. aber nicht etwas, von dem ich erwarten würde, dass ein Compiler es jemals erfinden würde; Ich würde auch nicht erwarten, dass ein Compiler ein Register "Vollzeit" für das Halten der Tabellenadresse reserviert.

Der obige Code wurde als eigenständiges System ausgeführt. Es konnte regelmäßig C-Code aufrufen, jedoch nur zu bestimmten Zeiten, zu denen die Hardware, mit der es kommunizierte, alle 16 ms für zwei Intervalle von ungefähr einer Millisekunde sicher in den Ruhezustand versetzt werden konnte.

Superkatze
quelle
2

In letzter Zeit haben alle Geschwindigkeitsoptimierungen, die ich vorgenommen habe, gehirngeschädigten langsamen Code durch nur vernünftigen Code ersetzt. Da die Geschwindigkeit jedoch sehr wichtig war und ich mich ernsthaft bemühte, etwas schnell zu machen, war das Ergebnis immer ein iterativer Prozess, bei dem jede Iteration mehr Einblick in das Problem gab und Wege fand, das Problem mit weniger Vorgängen zu lösen. Die endgültige Geschwindigkeit hing immer davon ab, wie viel Einblick ich in das Problem bekam. Wenn ich zu irgendeinem Zeitpunkt Assembler-Code oder C-Code verwendet hätte, der überoptimiert war, hätte der Prozess der Suche nach einer besseren Lösung gelitten und das Endergebnis wäre langsamer.

gnasher729
quelle
2

C ++ ist schneller, es sei denn, Sie verwenden Assemblersprache mit tieferen Kenntnissen auf die richtige Weise.

Wenn ich in ASM codiere, reorganisiere ich die Anweisungen manuell, damit die CPU mehr davon parallel ausführen kann, wenn dies logisch möglich ist. Ich verwende kaum RAM, wenn ich in ASM codiere, zum Beispiel: In ASM können mehr als 20000 Codezeilen vorhanden sein, und ich habe Push / Pop noch nie verwendet.

Sie könnten möglicherweise in die Mitte des Opcodes springen, um den Code und das Verhalten selbst zu ändern, ohne die mögliche Strafe eines sich selbst ändernden Codes. Der Zugriff auf Register erfordert 1 Tick (manchmal 0,25 Ticks) der CPU. Der Zugriff auf den RAM kann Hunderte dauern.

Bei meinem letzten ASM-Abenteuer habe ich den RAM nie zum Speichern einer Variablen verwendet (für Tausende von ASM-Zeilen). ASM könnte möglicherweise unvorstellbar schneller als C ++ sein. Aber es hängt von vielen variablen Faktoren ab, wie zum Beispiel:

1. I was writing my apps to run on the bare metal.
2. I was writing my own boot loader that was starting my programs in ASM so there was no OS management in the middle.

Ich lerne jetzt C # und C ++, weil mir klar wurde, dass Produktivität wichtig ist !! In der Freizeit können Sie versuchen, die schnellsten vorstellbaren Programme nur mit reinem ASM zu erstellen. Aber um etwas zu produzieren, verwenden Sie eine Hochsprache.

Zum Beispiel verwendete das letzte Programm, das ich codierte, JS und GLSL, und ich bemerkte nie ein Leistungsproblem, selbst wenn ich über JS sprach, das langsam ist. Dies liegt daran, dass das bloße Konzept der Programmierung der GPU für 3D die Geschwindigkeit der Sprache, die die Befehle an die GPU sendet, nahezu irrelevant macht.

Die Geschwindigkeit des Monteurs allein auf dem blanken Metall ist unwiderlegbar. Könnte es in C ++ noch langsamer sein? - Dies kann daran liegen, dass Sie Assembler-Code mit einem Compiler schreiben, der zunächst keinen Assembler verwendet.

Mein persönlicher Rat ist, niemals Assembler-Code zu schreiben, wenn Sie dies vermeiden können, obwohl ich Assembler liebe.


quelle
1

Alle Antworten hier scheinen einen Aspekt auszuschließen: Manchmal schreiben wir keinen Code, um ein bestimmtes Ziel zu erreichen, sondern nur zum Spaß . Es mag nicht wirtschaftlich sein, die Zeit dafür zu investieren, aber es gibt wohl keine größere Befriedigung, als das schnellste vom Compiler optimierte Code-Snippet in der Geschwindigkeit mit einer manuell gerollten asm-Alternative zu schlagen.

Madoki
quelle
Wenn Sie nur den Compiler schlagen möchten, ist es normalerweise einfacher, die ASM-Ausgabe für Ihre Funktion zu verwenden und daraus eine eigenständige ASM-Funktion zu machen, die Sie optimieren. Die Verwendung von Inline- ASM ist eine Menge zusätzlicher Arbeit, um die Schnittstelle zwischen C ++ und ASM korrekt zu machen und zu überprüfen, ob sie zu optimalem Code kompiliert wird. (Aber zumindest, wenn Sie es nur zum Spaß machen, müssen Sie sich keine Sorgen machen, dass Optimierungen wie die konstante Ausbreitung zunichte gemacht werden, wenn die Funktion in etwas anderes integriert wird. Gcc.gnu.org/wiki/DontUseInlineAsm ).
Peter Cordes
Weitere Informationen zum Schlagen des Compilers zum Spaß finden Sie in der Collatz-Vermutung C ++ vs. handgeschriebene asm-Fragen und Antworten :) Und auch Vorschläge, wie Sie das Gelernte verwenden können, um das C ++ zu ändern und dem Compiler zu helfen, besseren Code zu erstellen.
Peter Cordes
@PeterCordes Sie sagen also, Sie stimmen zu.
Madoki
1
Ja, asm macht Spaß, außer dass Inline- asm normalerweise die falsche Wahl ist, selbst wenn man herumspielt. Dies ist technisch gesehen eine Inline-Asm-Frage, daher wäre es gut, zumindest diesen Punkt in Ihrer Antwort anzusprechen. Dies ist auch eher ein Kommentar als eine Antwort.
Peter Cordes
In Ordnung, einverstanden. Früher war ich nur ein Asm, aber das waren die 80er Jahre.
Madoki
-2

Ein C ++ - Compiler würde nach der Optimierung auf Organisationsebene Code erzeugen, der die integrierten Funktionen der Ziel-CPU verwendet. HLL wird Assembler aus mehreren Gründen niemals überholen oder übertreffen. 1.) HLL wird kompiliert und mit Accessor-Code, Grenzprüfung und möglicherweise eingebauter Speicherbereinigung (früher Adressierung des Bereichs im OOP-Manierismus) ausgegeben, wobei alle Zyklen (Flips und Flops) erforderlich sind. HLL leistet heutzutage hervorragende Arbeit (einschließlich neuerer C ++ - und anderer wie GO), aber wenn sie den Assembler (nämlich Ihren Code) übertreffen, müssen Sie die CPU-Dokumentation konsultieren. Vergleiche mit schlampigem Code sind mit Sicherheit nicht schlüssig und kompiliert langsam wie Assembler HLL abstrahiert die Details und entfernt sie nicht. Andernfalls wird Ihre App nicht ausgeführt, wenn sie vom Host-Betriebssystem überhaupt erkannt wird.

Die meisten Assembler-Codes (hauptsächlich Objekte) werden als "kopflos" ausgegeben, um sie in andere ausführbare Formate aufzunehmen, wobei weitaus weniger Verarbeitung erforderlich ist. Daher ist sie viel schneller, aber weitaus unsicherer. Wenn eine ausführbare Datei vom Assembler (NAsm, YAsm usw.) ausgegeben wird, wird sie immer noch schneller ausgeführt, bis sie in ihrer Funktionalität vollständig mit dem HLL-Code übereinstimmt. Die Ergebnisse können dann genau abgewogen werden.

Das Aufrufen eines Assembler-basierten Codeobjekts aus HLL in einem beliebigen Format erhöht den Verarbeitungsaufwand zusätzlich zu Speicherplatzaufrufen, bei denen global zugewiesener Speicher für variable / konstante Datentypen verwendet wird (dies gilt sowohl für LLL als auch für HLL). Denken Sie daran, dass die endgültige Ausgabe die CPU letztendlich als API und Abi in Bezug auf die Hardware (Opcode) verwendet und sowohl Assembler als auch "HLL-Compiler" im Wesentlichen / grundlegend identisch sind, wobei die einzig wahre Ausnahme die Lesbarkeit (grammatikalisch) ist.

Die Hello World-Konsolenanwendung in Assembler mit FAsm ist 1,5 KB groß (und dies ist in Windows unter FreeBSD und Linux sogar noch kleiner) und übertrifft alles, was GCC an seinem besten Tag herausbringen kann. Gründe sind implizites Auffüllen mit Nops, Zugriffsüberprüfung und Grenzüberprüfung, um nur einige zu nennen. Das eigentliche Ziel sind saubere HLL-Bibliotheken und ein optimierbarer Compiler, der auf eine "Hardcore" -Methode abzielt, und die meisten tun dies heutzutage (endlich). GCC ist nicht besser als YAsm - es sind die Codierungspraktiken und das Verständnis des betreffenden Entwicklers, und die "Optimierung" erfolgt nach unerfahrener Erkundung und Zwischenschulung und Erfahrung.

Compiler müssen für die Ausgabe im selben Opcode wie ein Assembler verknüpft und zusammengesetzt werden, da diese Codes nur von einer CPU ausgenommen werden (CISC oder RISC [auch PIC]). YAsm optimierte und bereinigte viel auf frühem NAsm, was letztendlich die gesamte Ausgabe dieses Assemblers beschleunigte, aber selbst dann produziert YAsm wie NAsm im Auftrag des Entwicklers ausführbare Dateien mit externen Abhängigkeiten, die auf Betriebssystembibliotheken abzielen, sodass die Laufleistung variieren kann. Zum Schluss befindet sich C ++ an einem Punkt, der für mehr als 80 Prozent unglaublich und weitaus sicherer als Assembler ist, insbesondere im kommerziellen Bereich ...

Der Rabe
quelle
1
C und C ++ haben keine Grenzüberprüfung, es sei denn, Sie fragen danach, und keine Speicherbereinigung, es sei denn, Sie implementieren sie selbst oder verwenden eine Bibliothek. Die eigentliche Frage ist, ob der Compiler bessere Schleifen (und globale Optimierungen) als ein Mensch macht. Normalerweise ja, es sei denn, der Mensch weiß wirklich , was er tut und verbringt viel Zeit damit .
Peter Cordes
1
Sie können statische ausführbare Dateien mit NASM oder YASM erstellen (kein externer Code). Sie können beide im flachen Binärformat ausgeben, sodass Sie die ELF-Header selbst zusammenstellen können, wenn Sie sie wirklich nicht ausführen möchten. Dies ldmacht jedoch keinen Unterschied, es sei denn, Sie versuchen, die Dateigröße (nicht nur die Größe der Datei) wirklich zu optimieren Textsegment). Siehe ein Wirbelwind-Tutorial zum Erstellen von wirklich teensy ELF Executables für Linux .
Peter Cordes
1
Vielleicht denken Sie an C # oder std::vectorkompilieren im Debug-Modus. C ++ - Arrays sind nicht so. Compiler können Inhalte zur Kompilierungszeit überprüfen. Wenn Sie jedoch keine zusätzlichen Härtungsoptionen aktivieren, erfolgt keine Laufzeitprüfung. Siehe zum Beispiel eine Funktion, die die ersten 1024 Elemente eines int array[]Args inkrementiert . Die asm-Ausgabe hat keine Laufzeitprüfungen: godbolt.org/g/w1HF5t . Alles was es bekommt ist ein Zeiger in rdi, keine Größeninformationen. Es ist Sache des Programmierers, undefiniertes Verhalten zu vermeiden, indem er es niemals mit einem Array aufruft, das kleiner als 1024 ist.
Peter Cordes
1
Was auch immer Sie meinen, es handelt sich nicht um ein einfaches C ++ - Array (Zuweisen mit new, manuell löschen mit delete, keine Überprüfung der Grenzen). Sie können C ++ verwenden, um beschissenen, aufgeblähten asm / Maschinencode zu erzeugen (wie die meisten Softwareprogramme), aber das ist der Fehler des Programmierers, nicht der von C ++. Sie können sogar allocaStapelspeicher als Array zuweisen.
Peter Cordes
1
Verknüpfen Sie auf gcc.godbolt.org ein Beispiel für das Generieren von g++ -O3Code zur Überprüfung von Grenzen für ein einfaches Array oder für das, was Sie sonst noch tun. C ++ macht es viel einfacher, aufgeblähte Binärdateien zu generieren (und tatsächlich müssen Sie vorsichtig sein , wenn Sie keine Leistung anstreben), aber es ist nicht buchstäblich unvermeidlich. Wenn Sie verstehen, wie C ++ zu asm kompiliert wird, können Sie Code erhalten, der nur etwas schlechter ist, als Sie von Hand schreiben könnten, aber mit Inlining und konstanter Weitergabe über einen größeren Maßstab, als Sie von Hand verwalten könnten.
Peter Cordes
-3

Die Montage könnte schneller sein, wenn Ihr Compiler viel generiert OO- Supportcode generiert.

Bearbeiten:

An Downvoter: Das OP schrieb: "Soll ich ... mich auf C ++ konzentrieren und die Assemblersprache vergessen?" und ich stehe zu meiner Antwort. Sie müssen immer den Code im Auge behalten, den OO generiert, insbesondere wenn Sie Methoden verwenden. Wenn Sie die Assemblersprache nicht vergessen, überprüfen Sie regelmäßig die Assemblierung, die Ihr OO-Code generiert. Ich glaube, dies ist ein Muss für das Schreiben leistungsfähiger Software.

Tatsächlich bezieht sich dies auf den gesamten kompilierbaren Code, nicht nur auf OO.

Olof Forshell
quelle
2
-1: Ich sehe keine OO-Funktion. Ihr Argument ist dasselbe wie "Assembly könnte auch schneller sein, wenn Ihr Compiler eine Million NOPs hinzufügt."
Sjoerd
Mir war unklar, das ist eigentlich eine C-Frage. Wenn Sie C-Code für einen C ++ - Compiler schreiben, schreiben Sie keinen C ++ - Code und Sie erhalten kein OO-Zeug. Sobald Sie mit dem Schreiben in echtem C ++ beginnen und OO-Inhalte verwenden, müssen Sie über ausreichende Kenntnisse verfügen, damit der Compiler keinen OO-Supportcode generiert.
Olof Forshell
Ihre Antwort bezieht sich also nicht auf die Frage? (Auch Klarstellungen sind in der Antwort enthalten, keine Kommentare. Kommentare können jederzeit ohne Benachrichtigung, Benachrichtigung oder Verlauf gelöscht werden.
Mooing Duck
1
Nicht sicher, was genau Sie mit OO "Support Code" meinen. Wenn Sie viel RTTI und dergleichen verwenden, muss der Compiler natürlich viele zusätzliche Anweisungen erstellen, um diese Funktionen zu unterstützen. Jedes Problem, das auf hoher Ebene ausreicht, um die Verwendung von RTTI zu bestätigen, ist jedoch zu komplex, um in der Assembly geschrieben werden zu können . Was Sie natürlich tun können, ist, nur die abstrakte externe Schnittstelle als OO zu schreiben und an leistungsoptimierten reinen prozeduralen Code zu senden, wo dies kritisch ist. Abhängig von der Anwendung ist C, Fortran, CUDA oder einfach C ++ ohne virtuelle Vererbung möglicherweise besser als die Assembly hier.
links um den
2
Zumindest nicht sehr wahrscheinlich. In C ++ gibt es eine Sache, die als Null-Overhead-Regel bezeichnet wird, und dies gilt meistens. Erfahren Sie mehr über OO - Sie werden feststellen, dass es am Ende die Lesbarkeit Ihres Codes verbessert, die Codequalität verbessert, die Codierungsgeschwindigkeit erhöht und die Robustheit erhöht. Auch für Embedded - aber verwenden Sie C ++, da es Ihnen mehr Kontrolle gibt. Embedded + OO auf Java-Weise kostet Sie.
Zane