Sie haben Recht. Wenn Sie die Größe, die Sie in variabler Größe verwenden halten sub xxx, %rsp
, können Sie es umgekehrt mit einem add
am Ende (oder mit einem lea fixed_size(%rsp,%rdi,4), %rsp
auch ausplanen keiner festen Größe Stack-Platz reserviert.
Wie @Ross weist darauf hin, , dies skaliert nicht gut zu mehreren Zuweisungen mit variabler Länge in der gleichen Funktion. Selbst mit einem einzelnen VLA ist es nicht schneller als ein mov %rbp, %rsp
(oder leave
) am Ende der Funktion. Es würde den Compiler die Größe und verschütten lassen haben 15 freie Register anstelle von 14 für Teile der Funktion, die es nie mit %rbp
macht, wenn es als Rahmenzeiger verwendet wird. Wie auch immer, dies bedeutet, dass gcc immer noch auf einen Rahmenzeiger für komplexe Fälle zurückgreifen würde. Der Standardwert ist , aber machen Sie sich keine Sorgen dass es gcc nicht zwingt, niemals einen zu benutzen).
Mit %rbp
als ein Rahmenzeiger einige kleinere Vorteile, vor allem in Code-Größe hat: Ein addressing mode mit %rsp
als das Basisregister immer ein SIB-Byte (Skala/Index/Basis) benötigt, da die Mod/RM-Codierung das würde bedeuten, (%rsp)
ist eigentlich eine Escape-Sequenz, um anzuzeigen, dass es ein SIB-Byte gibt. In ähnlicher Weise bedeutet die Kodierung, die (%rbp)
ohne Verschiebung bedeutet, dass es überhaupt kein Basisregister gibt, so dass Sie immer ein disp8
Byte wie 0(%rbp)
benötigen.
Zum Beispiel ist mov %eax, 16(%rsp)
1B länger als mov %eax, -8(%rbp)
. Jan Hubicka suggested, dass es gut wäre, wenn gcc eine Heuristik hätte, um Rahmenzeiger in Funktionen zu aktivieren, in denen die Code-Größe gespeichert wurde, ohne Leistungsregressionen zu verursachen, und denkt, dass dies häufig der Fall ist. Es kann auch einige Stack-Sync-Ups speichern, um die Verwendung von %e/rsp
direkt (nach Push/Pop oder Call) auf Intel-CPUs mit einem Stack-Engine zu vermeiden.
GCC verwendet immer %rbp
als Rahmenzeiger in jeder Funktion mit C99 Arrays variabler Größe. Wahrscheinlich fanden gcc-Entwickler, dass es sich nicht lohnte, herauszufinden, wann eine solche Funktion ohne Frame-Pointer noch genauso effizient sein könnte, und in diesen seltenen Sonderfällen viel Code in gcc haben.
Aber was, wenn wir einen Rahmen mit Zeiger in einer Funktion mit einem VLA wirklich vermeiden wollten?
Das siebte und spätere Integer-Argument (in der SysV ABI, siehe das x86-Tag-Wiki) befindet sich auf dem Stapel über der Rücksendeadresse. Ein Zugriff über disp(%rsp)
ist nicht möglich, da die Verschiebung zur Kompilierzeit nicht bekannt ist.
disp(%rsp, %rcx, 1)
wäre möglich, wobei %rcx
die Variable Länge Array-Größe enthält. (Oder die Gesamtgröße aller VLAs). Dies kostet keine zusätzliche Codegröße über disp(%rsp)
, da Adressierungsmodi mit %rsp
als Basisregister bereits ein SIB-Byte verwenden müssen. Aber das bedeutet, dass die VLA-Größe in einem Register in Vollzeit bleiben muss, was uns nichts bringt, wenn ein Rahmenzeiger verwendet wird. (Und verlieren auf Code-Größe).
Die Alternative besteht darin, Skalare/Fixed-size Locals unterhalb von Zuordnungen mit variabler Länge zu halten, sodass wir immer auf sie mit einer festen Verschiebung relativ zu %rsp
zugreifen können. Das ist gut für die Code-Größe, da wir disp8
(1B) anstelle von disp32
(4B) verwenden können, um innerhalb von [-128, + 127] Bytes von %rsp
zuzugreifen.
Aber es funktioniert nur, wenn Sie die VLA Größe (n) früh bestimmen können, bevor Sie etwas an die Einheimischen verschütten müssen. Also haben Sie wieder einen komplexen Sonderfall, nach dem der Compiler sucht, und er benötigt in diesem Fall eine Menge Code-Generierungscode in gcc.
Wenn Sie die VLA-Größe verschütten und sie vor ret
urn erneut verwenden, machen Sie den Wert %rsp
abhängig von einem Neuladen aus dem Speicher. Out-of-Order-Ausführung kann diese zusätzliche Latenz wahrscheinlich verbergen, aber es gibt Fälle, in denen diese zusätzliche Latenz alles andere verzögert, was %rsp
verwendet, einschließlich der Wiederherstellung der Register des Aufrufers.
Diese Art von Code-Gen hätte wahrscheinlich auch einige Eckfälle für gcc, um damit korrekten und effizienten Code zu erstellen. Da es wenig benutzt wird, wird der "effiziente" Teil davon möglicherweise nicht viel Aufmerksamkeit bekommen.
Es ist ziemlich leicht zu sehen, warum gcc einfach in den Frame-Pointer-Modus zurückfällt, wenn es nicht einfach ist, es wegzulassen. Normalerweise erhält man fast kostenlos ein zusätzliches Register, daher lohnt es sich, den Code-Größenvorteil aufzugeben, auch wenn man viele Einheimische anspricht. Dies gilt insbesondere für 32-Bit-Code, wo Sie von 6 bis 7 allgemeine Register gehen (ohne esp
). Dieser Unterschied ist normalerweise bei 64-Bit-Code geringer, wobei 14 gegenüber 15 ein viel kleinerer Unterschied ist. Es speichert immer noch die Push/mov/pop Anweisungen in Funktionen, die sie nicht benötigen, was ein separater Vorteil ist. (Die Verwendung von %rbp
als Allzweckregister erfordert immer noch das Drücken/Öffnen.)
Es ist eine weit verbreitete Konvention, die von einigen ABIs verlangt wird. Aber wenn Sie Ihr eigenes ABI erfinden, dann müssen Sie '% rbp' nicht verwenden. (Das heißt, '% rbp' ist eine gute Wahl, da es für' (% rbp) 'keinen Adressierungsmodus gibt, Sie müssen' 0 (% rbp) 'verwenden. Dies macht'% rbp' zu einer schlechten Wahl für einen General Zweck-Zeiger, aber es ist okay als ein Rahmenzeiger, weil Sie niemals auf '(% rbp)' zugreifen müssen, da alles, was es enthält, der vorherige Rahmenzeiger ist.) –
Es gibt auch eine 'Leave'-Anweisung, die' mov% rbp,% rsp '/' pop% rbp'. Es ist 3 Ups auf Intel, vs 2 Ups für die gleiche Sache "manuell", aber es ist nur 1 Byte. –
Es verwendet auch implizit 'ss' als Selektor/Segment und war eines der wenigen im Realmodus verfügbaren Basisregister. –