2015-07-21 5 views
5

Eine triviale Funktion mit gcc und Klirren ich kompilieren:Warum verwenden llvm und gcc verschiedene Funktionsprotokolle auf x86 64?

void test() { 
    printf("hm"); 
    printf("hum"); 
} 


$ gcc test.c -fomit-frame-pointer -masm=intel -O3 -S 

sub rsp, 8 
.cfi_def_cfa_offset 16 
mov esi, OFFSET FLAT:.LC0 
mov edi, 1 
xor eax, eax 
call __printf_chk 
mov esi, OFFSET FLAT:.LC1 
mov edi, 1 
xor eax, eax 
add rsp, 8 
.cfi_def_cfa_offset 8 
jmp __printf_chk 

Und

$ clang test.c -mllvm --x86-asm-syntax=intel -fomit-frame-pointer -O3 -S  

# BB#0: 
push rax 
.Ltmp1: 
.cfi_def_cfa_offset 16 
mov edi, .L.str 
xor eax, eax 
call printf 
mov edi, .L.str1 
xor eax, eax 
pop rdx 
jmp printf     # TAILCALL 

Der Unterschied mich interessiert ist, dass gcc sub rsp, 8 verwendet/add rsp, 8 für die Funktion Prolog und Klang verwendet push rax/pop rdx.

Warum verwenden die Compiler unterschiedliche Funktionsprologe? Welche Variante ist besser? push und pop codiert sicherlich zu kürzeren Anweisungen aber sind sie schneller oder langsamer als add und sub?

Der Grund für das Stack-Fiddling an erster Stelle scheint zu sein, dass das ABI erfordert, dass rsp 16 Bytes für Non-Leaf-Prozeduren ist. Ich konnte keine Compilerflags finden, die sie entfernen.

Ausgehend von Ihren Antworten scheint es, Push & Pop ist besser. push rax + pop rdx = 1 + 1 = 2 vs. sub rsp, 8 + add rsp, 8 = 4 + 4 = 8. So spart das ehemalige Paar 6 Bytes ohne Kosten.

+0

Es ist eine Frage der Wahl. Es ist schwer zu sagen, welche Variante besser ist. Wahrscheinlich sind beide Varianten in Bezug auf die Leistung ziemlich ähnlich. –

+0

re: Ihre Bearbeitung. Ja, der ABI garantiert, dass bei Funktionseingabe "(% rsp + 8)" 16B ausgerichtet ist. (Ich habe den Großteil dieses Kommentars in meine Antwort übernommen). –

Antwort

8

Auf Intel, sub/add den Stapel Motor auslösen einen zusätzlichen einfügen uop, um %rsp für den Out-of-Order-Ausführungsteil der Pipeline zu synchronisieren. (Siehe Agner Fog's microarch doc, insbesondere S. 91, um den Stapel Motor. AFAIK, es funktioniert immer noch das gleiche auf Haswell wie auf Pentium M, bis wann braucht es zusätzliche Uops einzufügen.

Die push/pop nehmen weniger verschmolzenen -domain ups, und so wahrscheinlich effizienter sein, auch wenn sie die Laden/Laden-Ports verwenden. Sie kommen zwischen Call/Ret-Paare.

So push/pop wird zumindest nicht langsamer sein, aber nimmt weniger Anweisungen Bessere I-Cache-Dichte ist gut

BTW, ich denke, der Punkt des Paares von Insns ist, den Stapel 16B-ausgerichtet af zu halten ter call drückt die 8B Rücksprungadresse. Dies ist ein Fall, in dem die ABI letztendlich halbnotwendige Anweisungen benötigt. Komplexere Funktionen, die Speicherplatz benötigen, um Locals zu verschütten, und diese dann nach Funktionsaufrufen neu laden, werden normalerweise sub $something, %rsp Speicherplatz reservieren.

Die SystemV (Linux) -Amd64 ABI garantiert, dass bei Funktionseingabe (%rsp + 8), wo Args auf dem Stapel sein werden, wenn es welche gibt, 16B ausgerichtet wird. (http://x86-64.org/documentation/abi.pdf). Sie müssen dafür sorgen, dass dies bei jeder Funktion der Fall ist, die Sie aufrufen, oder es ist Ihre Schuld, wenn sie die Verwendung einer SSE-ausgerichteten Last ausschließen. Oder andernfalls stürzen Sie sich auf Annahmen, wie sie AND verwenden können, um eine Adresse oder etwas zu maskieren.

+0

Ja, dies dient nur dazu, den Stack ausgerichtet zu halten. – WhatsUp

+1

Beachten Sie auch, dass die meisten Zeitfunktionen etwas Platz für lokale Variablen zuweisen, und die 'sub'-Variante ist in diesem Fall effizienter. Vermutlich haben sich die Compiler-Autoren nicht für den Fall optimiert, dass keine Einheimischen benötigt werden. – Jester

+0

Ja, Nicht-Blatt-Funktionen mit sehr wenigen Einheimischen ist ein seltener Fall. Ich denke, dass clangs Verwendung von 'push' /' pop' von Daten, die es nicht interessiert, eine saubere Optimierung ist. –

1

Nach den Experimenten, die ich auf meiner Maschine durchgeführt habe, haben push/pop die gleiche Geschwindigkeit wie add/sub. Ich denke, es sollte für alle modernen Computer der Fall sein.

Wie auch immer, der Unterschied (falls vorhanden) ist wirklich Mikro-pischen, so schlage ich vor, Sie sicher davon ausgehen, dass sie gleichwertig sind ...

+0

Welche Art von Experiment? Haben Sie etwas getestet, bei dem der Durchsatz beim Durchsatz zu hoch war? Ich stimme zu, dass es wahrscheinlich die meiste Zeit keinen Unterschied gibt. –

+0

Ich machte das naivste: kopiere eine Anweisung mehrere tausend Mal (tatsächlich mit Makros), setze das Ganze in eine Schleife und renne. Ich bin mir nicht sicher, ob dies auf Hochwasser beschränkt ist. Kannst du bestätigen? – WhatsUp

+0

'Add' mit den gleichen Registern jedes Mal benötigt die Ausgabe der vorherigen als ein Eingang, wodurch Latenz der Begrenzer. 'add' hat einen Durchsatz von 3 pro Zyklus auf SnB/IvB und 4 pro Zyklus auf Haswell, wenn sie unabhängig sind. 'push' kann 1/Zyklus aushalten,' pop' 2/Zyklus. Wie immer bei modernen CPUs kommt es auf den Kontext an (was andere Insins um Ausführungsressourcen konkurrieren und wie sie in eine Abhängigkeitskette passen). –