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++11
und 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?
c++
gcc
assembly
optimization
inline-assembly
R. Martinho Fernandes
quelle
quelle
asm
Formular mit leeren Ausgabe- und Clobber-Listen aus.asm("# im in ur loop" : : );
(siehe Dokumentation )-fverbose-asm
Flag hinzufügen, das einige Anmerkungen hinzufügt, um festzustellen, wie sich die Dinge zwischen den Registern bewegen.Antworten:
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:
und
Das Vorhandensein des
asm
Inneren Ihrer Schleife hat also eine Vektorisierungsoptimierung verhindert, da GCC davon ausgeht, dass es Nebenwirkungen hat.quelle
asm
Anweisung 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 emittierenpaddb
. Das würde ich allerdings für legal halten, da die Char-Zugriffe dies nicht sindvolatile
. (Anders als bei einer erweiterten asm-Anweisung mit einem"memory"
Clobber)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.
quelle
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 sogarasm()
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 haben
cpuid
Das ä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.quelle
volatile asm
teilt 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).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 dieprintf
Verwendung einer Formatzeichenfolge. TL: DR: Verwenden Sie GNU C Basic asm nicht für irgendetwas, außer für solche Anwendungsfälle mit reinen Kommentaren.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
(eineasm
Anweisung 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 EDITSie 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.
quelle
"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. Selbstasm("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).add rsp, -128
zuerst. Aber das zu tun ist offensichtlich verrückt.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%operand
die 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.