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 Costs

Kosten 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.

Rückgabewert kostet

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.

Antworten auf die Frage(2)

Ihre Antwort auf die Frage