Ist in x86-64 SysV ABI Müll in hohen Bits von Parameter- und Rückgabewertregistern zulässig?
Das x86-64 SysV ABI legt unter anderem fest, wie Funktionsparameter in Registern übergeben werden (erstes Argument inrdi
, dannrsi
und so weiter) und wie ganzzahlige Rückgabewerte zurückgegeben werden (inrax
und dannrdx
für wirklich große Werte).
Was ich jedoch nicht finden kann, ist, wie hoch die Bits der Parameter- oder Rückgabewertregister sein sollten, wenn Typen übergeben werden, die kleiner als 64 Bit sind.
Zum Beispiel für die folgende Funktion:
void foo(unsigned x, unsigned y);
...x
wird übergeben inrdi
undy
imrsi
, aber sie sind nur 32-Bit. Mach die hohen 32-Bit vonrdi
undrsi
muss Null sein? Intuitiv würde ich ja annehmen, aber dascode generated von allen gcc, clang und icc hat spezifischemov
-Anweisungen am Anfang setzen die High-Bits auf Null, so dass die Compiler anscheinend etwas anderes annehmen.
Ebenso scheinen die Compiler anzunehmen, dass die High-Bits des Rückgabewertsrax
kann Garbage Bits enthalten, wenn der Rückgabewert kleiner als 64 Bits ist. Zum Beispiel die Schleifen im folgenden Code:
unsigned gives32();
unsigned short gives16();
long sum32_64() {
long total = 0;
for (int i=1000; i--; ) {
total += gives32();
}
return total;
}
long sum16_64() {
long total = 0;
for (int i=1000; i--; ) {
total += gives16();
}
return total;
}
...kompiliere zu den folgenden inclang
(und andere Compiler sind ähnlich):
sum32_64():
...
.LBB0_1:
call gives32()
mov eax, eax
add rbx, rax
inc ebp
jne .LBB0_1
sum16_64():
...
.LBB1_1:
call gives16()
movzx eax, ax
add rbx, rax
inc ebp
jne .LBB1_1
Beachten Sie dasmov eax, eax
nach dem Aufruf, der 32-Bit zurückgibt, und dasmovzx eax, ax
nach dem 16-Bit-Aufruf - beide bewirken, dass die oberen 32 bzw. 48 Bits auf Null gesetzt werden. Dieses Verhalten hat also einige Kosten - die gleiche Schleife, die sich mit einem 64-Bit-Rückgabewert befasst, lässt diese Anweisung aus.
Ich habe das @ geles x86-64 System V ABI-Dokument ziemlich genau, aber ich konnte nicht feststellen, ob dieses Verhalten im Standard dokumentiert ist.
Was sind die Vorteile einer solchen Entscheidung? Es scheint mir, dass es klare Kosten gibt:
Parameter CostsKosten fallen bei der Implementierung von callee an, wenn es um Parameterwerte geht. und in den Funktionen beim Umgang mit den Parametern. Zugegeben, oft sind diese Kosten Null, weil die Funktion die hohen Bits effektiv ignorieren kann, oder die Nullsetzung ist kostenlos, da 32-Bit-Operandengrößenbefehle verwendet werden können, die die hohen Bits implizit auf Null setze
Bei Funktionen, die 32-Bit-Argumente akzeptieren und mit 64-Bit-Mathematik rechnen, sind die Kosten jedoch häufig sehr real. Nehmendiese Funktion zum Beispiel
uint32_t average(uint32_t a, uint32_t b) {
return ((uint64_t)a + b) >> 2;
}
Eine einfache Verwendung von 64-Bit-Mathematik zur Berechnung einer Funktion, die sonst sorgfältig mit Überlauf umgehen müsste (die Möglichkeit, viele 32-Bit-Funktionen auf diese Weise zu transformieren, ist ein oft unbemerkteter Vorteil von 64-Bit-Architekturen). Dies kompiliert zu:
average(unsigned int, unsigned int):
mov edi, edi
mov eax, esi
add rax, rdi
shr rax, 2
ret
2 von 4 Anweisungen vollständig ausfüllen (@ ignorierret
) werden nur benötigt, um die hohen Bits auf Null zu setzen. Dies mag in der Praxis mit Mov-Elimination billig sein, aber dennoch scheint es ein großer Aufwand zu sein, ihn zu bezahlen.
Andererseits kann ich keine ähnlichen entsprechenden Kosten für die Anrufer sehen, wenn die ABI spezifizieren würde, dass hohe Bits Null sind. Weilrdi
undrsi
und die anderen Parameterübergaberegister sindkratze (d. h. kann vom Anrufer überschrieben werden), es gibt nur ein paar Szenarien (wir betrachtenrdi
, aber ersetzen Sie es durch den Parameter reg Ihrer Wahl):
Der an die Funktion in @ übergebene Werdi
ist im Post-Call-Code tot (nicht erforderlich). In diesem Fall wird die zuletzt @ zugewiesene Anweisurdi
muss nur @ zugewiesen werdedi
stattdessen. Dies ist nicht nur kostenlos, es ist oft ein Byte kleiner, wenn Sie ein REX-Präfix vermeiden.
Der an die Funktion in @ übergebene Werdi
ist wird nach der Funktion benötigt. In diesem Fall, dardi
ist vom Anrufer gespeichert, der Anrufer muss ein @ machmov
des Wertes auf jeden Fall an ein von Angerufenen gespeichertes Register. Sie können es generell so organisieren, dass der Wert startet im gespeicherten Register der Angerufenen (sagen Sierbx
) und wird dann nach @ verschobedi
mögenmov edi, ebx
, also kostet es nichts.
Ich kann nicht viele Szenarien sehen, in denen das Nullsetzen den Anrufer viel kostet. Einige Beispiele wären, wenn 64-Bit-Mathematik in der letzten Anweisung benötigt wird, die @ zugewiesen hardi
. Das kommt mir allerdings recht selten vor.
Hier scheint die entscheidung neutraler zu sein. Wenn Callees den Müll beseitigt haben, haben sie einen bestimmten Code (manchmal sehen Siemov eax, eax
Anleitung dazu), aber wenn Müll erlaubt ist, verlagern sich die Kosten auf den Angerufenen. Insgesamt scheint es wahrscheinlicher zu sein, dass der Anrufer den Müll kostenlos beseitigen kann. Daher wirkt sich das Zulassen von Müll insgesamt nicht nachteilig auf die Leistung aus.
Ich nehme an, ein interessanter Anwendungsfall für dieses Verhalten ist, dass Funktionen mit unterschiedlichen Größen eine identische Implementierung gemeinsam nutzen können. Zum Beispiel alle folgenden Funktionen:
short sums(short x, short y) {
return x + y;
}
int sumi(int x, int y) {
return x + y;
}
long suml(long x, long y) {
return x + y;
}
Kann tatsächlich die gleiche Implementierung teilen1:
sum:
lea rax, [rdi+rsi]
ret
1 Ob eine solche Faltung ist eigentlichdürfe für Funktionen, die ihre Adresse haben, ist sehr viel offen für Diskussionen.