Warum führt das Hinzufügen von Inline-Assembly-Kommentaren zu solch radikalen Änderungen im generierten Code von GCC?

82

Also hatte ich diesen Code:

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
    for(int i = 0; i < N; ++i) {
        sum[i] = a[i] + b[i];
    }
}

void f2(char* sum, char* a, char* b) {
    char* end = sum + N;
    while(sum != end) {
        *sum++ = *a++ + *b++;
    }
}

Ich wollte den Code sehen, den GCC 4.7.2 generieren würde. Also lief ich g++ -march=native -O3 -masm=intel -S a.c++ -std=c++11und bekam folgende Ausgabe:

        .file   "a.c++"
        .intel_syntax noprefix
        .text
        .p2align 4,,15
        .globl  _Z2f1PcS_S_
        .type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
        .cfi_startproc
        lea     rcx, [rdx+16]
        lea     rax, [rdi+16]
        cmp     rdi, rcx
        setae   r8b
        cmp     rdx, rax
        setae   cl
        or      cl, r8b
        je      .L5
        lea     rcx, [rsi+16]
        cmp     rdi, rcx
        setae   cl
        cmp     rsi, rax
        setae   al
        or      cl, al
        je      .L5
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L3:
        movdqu  xmm0, XMMWORD PTR [rdx+rax]
        movdqu  xmm1, XMMWORD PTR [rsi+rax]
        paddb   xmm0, xmm1
        movdqu  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, 992
        jne     .L3
        mov     ax, 8
        mov     r9d, 992
.L2:
        sub     eax, 1
        lea     rcx, [rdx+r9]
        add     rdi, r9
        lea     r8, [rax+1]
        add     rsi, r9
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L4:
        movzx   edx, BYTE PTR [rcx+rax]
        add     dl, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], dl
        add     rax, 1
        cmp     rax, r8
        jne     .L4
        rep
        ret
.L5:
        mov     eax, 1000
        xor     r9d, r9d
        jmp     .L2
        .cfi_endproc
.LFE0:
        .size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
        .p2align 4,,15
        .globl  _Z2f2PcS_S_
        .type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
        .cfi_startproc
        lea     rcx, [rdx+16]
        lea     rax, [rdi+16]
        cmp     rdi, rcx
        setae   r8b
        cmp     rdx, rax
        setae   cl
        or      cl, r8b
        je      .L19
        lea     rcx, [rsi+16]
        cmp     rdi, rcx
        setae   cl
        cmp     rsi, rax
        setae   al
        or      cl, al
        je      .L19
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L17:
        movdqu  xmm0, XMMWORD PTR [rdx+rax]
        movdqu  xmm1, XMMWORD PTR [rsi+rax]
        paddb   xmm0, xmm1
        movdqu  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, 992
        jne     .L17
        add     rdi, 992
        add     rsi, 992
        add     rdx, 992
        mov     r8d, 8
.L16:
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L18:
        movzx   ecx, BYTE PTR [rdx+rax]
        add     cl, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], cl
        add     rax, 1
        cmp     rax, r8
        jne     .L18
        rep
        ret
.L19:
        mov     r8d, 1000
        jmp     .L16
        .cfi_endproc
.LFE1:
        .size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
        .ident  "GCC: (GNU) 4.7.2"
        .section        .note.GNU-stack,"",@progbits

Ich lutsche am Lesen der Versammlung, also habe ich beschlossen, einige Markierungen hinzuzufügen, um zu wissen, wohin die Körper der Schleifen gingen:

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
    for(int i = 0; i < N; ++i) {
        asm("# im in ur loop");
        sum[i] = a[i] + b[i];
    }
}

void f2(char* sum, char* a, char* b) {
    char* end = sum + N;
    while(sum != end) {
        asm("# im in ur loop");
        *sum++ = *a++ + *b++;
    }
}

Und GCC spuckte dies aus:

    .file   "a.c++"
    .intel_syntax noprefix
    .text
    .p2align 4,,15
    .globl  _Z2f1PcS_S_
    .type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
    .cfi_startproc
    xor eax, eax
    .p2align 4,,10
    .p2align 3
.L2:
#APP
# 4 "a.c++" 1
    # im in ur loop
# 0 "" 2
#NO_APP
    movzx   ecx, BYTE PTR [rdx+rax]
    add cl, BYTE PTR [rsi+rax]
    mov BYTE PTR [rdi+rax], cl
    add rax, 1
    cmp rax, 1000
    jne .L2
    rep
    ret
    .cfi_endproc
.LFE0:
    .size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
    .p2align 4,,15
    .globl  _Z2f2PcS_S_
    .type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
    .cfi_startproc
    xor eax, eax
    .p2align 4,,10
    .p2align 3
.L6:
#APP
# 12 "a.c++" 1
    # im in ur loop
# 0 "" 2
#NO_APP
    movzx   ecx, BYTE PTR [rdx+rax]
    add cl, BYTE PTR [rsi+rax]
    mov BYTE PTR [rdi+rax], cl
    add rax, 1
    cmp rax, 1000
    jne .L6
    rep
    ret
    .cfi_endproc
.LFE1:
    .size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
    .ident  "GCC: (GNU) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

Dies ist erheblich kürzer und weist einige signifikante Unterschiede auf, wie das Fehlen von SIMD-Anweisungen. Ich hatte die gleiche Ausgabe erwartet, mit einigen Kommentaren irgendwo in der Mitte. Mache ich hier eine falsche Annahme? Wird der Optimierer von GCC durch asm-Kommentare behindert?

R. Martinho Fernandes
quelle
28
Ich würde erwarten, dass GCC (und die meisten Compiler) ASM-Konstrukte wie Blockboxen behandeln. Sie können also nicht darüber nachdenken, was durch eine solche Box passiert. Und das verhindert viele Optimierungen, insbesondere solche, die über Schleifengrenzen hinweg übertragen werden.
Ira Baxter
10
Probieren Sie das erweiterte asmFormular mit leeren Ausgabe- und Clobber-Listen aus.
Kerrek SB
4
@ R.MartinhoFernandes: asm("# im in ur loop" : : );(siehe Dokumentation )
Mike Seymour
16
Beachten Sie, dass Sie beim Betrachten der generierten Assembly etwas mehr Hilfe erhalten können, indem Sie das -fverbose-asmFlag hinzufügen, das einige Anmerkungen hinzufügt, um festzustellen, wie sich die Dinge zwischen den Registern bewegen.
Matthew Slattery
1
Sehr interessant. Kann verwendet werden, um eine Optimierung in Schleifen selektiv zu vermeiden?
SChepurin

Antworten:

61

Die Wechselwirkungen mit Optimierungen werden etwa in der Mitte der Seite "Assembler-Anweisungen mit C-Ausdrucksoperanden" in der Dokumentation erläutert .

GCC versucht nicht, die tatsächliche Baugruppe innerhalb der zu verstehen asm. Das einzige, was es über den Inhalt weiß, ist das, was Sie (optional) in der Ausgabe- und Eingabeoperandenspezifikation und in der Register-Clobber-Liste angeben.

Beachten Sie insbesondere:

Ein asmBefehl ohne Ausgangsoperanden wird identisch mit einem flüchtigen asmBefehl behandelt.

und

Das volatileSchlüsselwort zeigt an, dass die Anweisung wichtige Nebenwirkungen hat [...]

Das Vorhandensein des asmInneren Ihrer Schleife hat also eine Vektorisierungsoptimierung verhindert, da GCC davon ausgeht, dass es Nebenwirkungen hat.

Matthew Slattery
quelle
1
Beachten Sie, dass die Nebenwirkungen einer Basic Asm-Anweisung nicht das Ändern von Registern oder Speicher umfassen dürfen, den Ihr C ++ - Code jemals liest / schreibt. Aber ja, die asmAnweisung muss jedes Mal einmal ausgeführt werden, wenn dies in der abstrakten C ++ - Maschine der Fall ist, und GCC entscheidet sich dafür, den asm nicht zu vektorisieren und dann 16 Mal hintereinander pro zu emittieren paddb. Das würde ich allerdings für legal halten, da die Char-Zugriffe dies nicht sind volatile. (Anders als bei einer erweiterten asm-Anweisung mit einem "memory"Clobber)
Peter Cordes
1
Unter gcc.gnu.org/wiki/ConvertBasicAsmToExtended finden Sie Gründe, warum GNU C Basic Asm-Anweisungen im Allgemeinen nicht verwendet werden. Obwohl dieser Anwendungsfall (nur ein Kommentar) einer der wenigen ist, bei denen es nicht unangemessen ist, ihn auszuprobieren.
Peter Cordes
23

Beachten Sie, dass gcc den Code vektorisierte und den Schleifenkörper in zwei Teile aufteilte, wobei der erste 16 Elemente gleichzeitig verarbeitete und der zweite den Rest später erledigte.

Wie Ira kommentierte, analysiert der Compiler den asm-Block nicht, sodass er nicht weiß, dass es sich nur um einen Kommentar handelt. Selbst wenn dies der Fall ist, kann es nicht wissen, was Sie beabsichtigt haben. Die optmisierten Schleifen haben den Körper verdoppelt, sollte es Ihren Asm in jeden setzen? Möchten Sie, dass es nicht 1000 Mal ausgeführt wird? Es weiß es nicht, also geht es den sicheren Weg und greift auf die einfache Einzelschleife zurück.

Narr
quelle
2

Ich bin nicht einverstanden mit dem "gcc versteht nicht, was in dem asm()Block ist". Zum Beispiel kann gcc recht gut mit der Optimierung von Parametern umgehen und sogar asm()Blöcke so neu anordnen , dass sie sich mit dem generierten C-Code vermischen. Wenn Sie sich beispielsweise den Inline-Assembler im Linux-Kernel ansehen, wird ihm fast immer ein Präfix vorangestellt__volatile__ um sicherzustellen, dass der Compiler "den Code nicht verschiebt". Ich habe gcc mein "rdtsc" bewegen lassen, wodurch ich die Zeit gemessen habe, die es dauerte, um bestimmte Dinge zu tun.

Wie dokumentiert, behandelt gcc bestimmte Arten von asm() Blöcken als "speziell" und optimiert daher den Code auf beiden Seiten des Blocks nicht.

Das heißt nicht, dass gcc manchmal nicht durch Inline-Assembler-Blöcke verwirrt wird oder einfach beschließt, eine bestimmte Optimierung aufzugeben, da es den Konsequenzen des Assembler-Codes usw. usw. nicht folgen kann. Noch wichtiger ist, dass dies nicht der Fall ist kann oft durch fehlende Clobber-Tags verwirrt werden - wenn Sie also eine Anleitung wie habencpuidDas ändert den Wert von EAX-EDX, aber Sie haben den Code so geschrieben, dass er nur EAX verwendet. Der Compiler kann Dinge in EBX, ECX und EDX speichern, und dann verhält sich Ihr Code sehr seltsam, wenn diese Register überschrieben werden ... Wenn Sie haben Glück, es stürzt sofort ab - dann ist es einfach herauszufinden, was los ist. Aber wenn Sie Pech haben, stürzt es auf der ganzen Linie ab ... Eine andere schwierige ist die Divide-Anweisung, die in edx ein zweites Ergebnis liefert. Wenn Sie sich nicht für das Modulo interessieren, können Sie leicht vergessen, dass EDX geändert wurde.

Mats Petersson
quelle
1
gcc versteht wirklich nicht, was sich im asm-Block befindet - Sie müssen es über eine erweiterte asm-Anweisung mitteilen. Ohne diese zusätzlichen Informationen bewegt sich gcc nicht um solche Blöcke. gcc wird auch in den von Ihnen angegebenen Fällen nicht verwirrt - Sie haben einfach einen Programmierfehler gemacht, indem Sie gcc mitgeteilt haben, dass es diese Register verwenden kann, obwohl Ihr Code sie tatsächlich blockiert.
Erinnern Sie sich an Monica
Späte Antwort, aber ich denke, es lohnt sich zu sagen. volatile asmteilt GCC mit, dass der Code möglicherweise „wichtige Nebenwirkungen“ hat, und er wird mit besonderer Sorgfalt damit umgehen. Es kann weiterhin im Rahmen der Dead-Code-Optimierung gelöscht oder verschoben werden. Die Interaktion mit C-Code muss einen solchen (seltenen) Fall annehmen und eine strikte sequentielle Bewertung auferlegen (z. B. durch Erstellen von Abhängigkeiten innerhalb des ASM).
Edmz
GNU C Basic asm (keine Operandeneinschränkungen wie die OPs asm("")) ist implizit flüchtig, genau wie Extended asm ohne Ausgabeoperanden. GCC versteht die ASM-Vorlagenzeichenfolge nicht, nur die Einschränkungen. Aus diesem Grund ist es wichtig , dem Compiler Ihren Asm mithilfe von Einschränkungen genau und vollständig zu beschreiben. Das Einsetzen von Operanden in die Vorlagenzeichenfolge erfordert nicht mehr Verständnis als die printfVerwendung einer Formatzeichenfolge. TL: DR: Verwenden Sie GNU C Basic asm nicht für irgendetwas, außer für solche Anwendungsfälle mit reinen Kommentaren.
Peter Cordes
-2

Diese Antwort wurde jetzt geändert: Sie wurde ursprünglich mit einer Denkweise geschrieben, die Inline Basic Asm als ein ziemlich stark spezifiziertes Tool betrachtet, aber in GCC ist das nicht so. Basic Asm ist schwach und daher wurde die Antwort bearbeitet.

Jeder Assemblykommentar fungiert als Haltepunkt.

EDIT: Aber eine kaputte, wenn Sie Basic Asm verwenden. Inline asm(eine asmAnweisung innerhalb eines Funktionskörpers) ohne explizite Clobber-Liste ist eine schwach spezifizierte Funktion in GCC und ihr Verhalten ist schwer zu definieren. Es ist nicht scheint (ich nicht in vollem Umfang seine Garantien erfassen) , die an etwas Bestimmtes, so dass während des Assembler - Code zu einem bestimmten Zeitpunkt ausgeführt werden muss , wenn die Funktion ausgeführt wird, es ist nicht klar , wenn es für alle nicht ausgeführt wird triviale Optimierungsstufe . Ein Haltepunkt, der mit benachbarten Anweisungen neu angeordnet werden kann, ist kein sehr nützlicher "Haltepunkt". END EDIT

Sie können Ihr Programm in einem Interpreter ausführen, der bei jedem Kommentar unterbrochen wird und den Status jeder Variablen ausgibt (unter Verwendung von Debug-Informationen). Diese Punkte müssen vorhanden sein, damit Sie die Umgebung beobachten können (Status der Register und des Speichers).

Ohne den Kommentar existiert kein Beobachtungspunkt, und die Schleife wird als einzelne mathematische Funktion kompiliert, die eine Umgebung nimmt und eine modifizierte Umgebung erzeugt.

Sie möchten die Antwort auf eine bedeutungslose Frage wissen: Sie möchten wissen, wie jeder Befehl (oder vielleicht Block oder vielleicht ein Befehlsbereich) kompiliert wird, aber kein einzelner isolierter Befehl (oder Block) wird kompiliert. Das ganze Zeug wird als Ganzes zusammengestellt.

Eine bessere Frage wäre:

Hallo GCC. Warum implementiert diese asm-Ausgabe Ihrer Meinung nach den Quellcode? Bitte erklären Sie Schritt für Schritt mit jeder Annahme.

Aber dann möchten Sie keinen Proof länger lesen als die asm-Ausgabe, die als interne GCC-Darstellung geschrieben wurde.

Neugieriger
quelle
1
Diese Punkte müssen vorhanden sein, damit Sie die Umgebung beobachten können (Status der Register und des Speichers). - Dies gilt möglicherweise für nicht optimierten Code. Bei aktivierten Optimierungen können ganze Funktionen aus der Binärdatei verschwinden. Wir sprechen hier von optimiertem Code.
Bartek Banachewicz
1
Wir sprechen von Assemblys, die durch Kompilieren mit aktivierten Optimierungen generiert wurden. Daher sagen Sie zu Unrecht , dass irgendetwas existieren muss.
Bartek Banachewicz
1
Ja, IDK, warum irgendjemand jemals würde, und stimme zu, dass niemand jemals sollte. Wie der Link in meinem letzten Kommentar erklärt, sollte das "memory"niemals jemand tun , und es gab Debatten darüber, ihn (z. B. mit einem impliziten Clobber) als Pflaster für den vorhandenen Buggy-Code zu stärken, der sicherlich existiert. Selbst asm("cli")wenn Anweisungen wie diese nur einen Teil des Architekturstatus betreffen, den der vom Compiler generierte Code nicht berührt, müssen Sie ihn dennoch in der Reihenfolge bestellen. Vom Compiler generierte Ladevorgänge / Speicher (z. B. wenn Sie Interrupts in einem kritischen Abschnitt deaktivieren).
Peter Cordes
1
Da es nicht sicher ist, die rote Zone zu blockieren, ist selbst ein ineffizientes manuelles Speichern / Wiederherstellen von Registern (mit Push / Pop) in der asm-Anweisung nicht sicher, es sei denn, Sie add rsp, -128zuerst. Aber das zu tun ist offensichtlich verrückt.
Peter Cordes
1
Derzeit behandelt GCC Basic Asm genau gleichbedeutend mit asm("" :::)(implizit flüchtig, da es keine Ausgaben hat, aber nicht durch Eingabe- oder Ausgabeabhängigkeiten an den Rest des Codes gebunden ist. Und kein "memory"Clobber). Und natürlich wird %operanddie Vorlagenzeichenfolge nicht ersetzt, sodass das Literal %nicht als maskiert werden muss %%. Also ja, einverstanden, Basic Asm außerhalb von __attribute__((naked))Funktionen und globalem Geltungsbereich abzulehnen, wäre eine gute Idee.
Peter Cordes