¿Se permite la basura en bits altos de parámetros y registros de valor de retorno en x86-64 SysV ABI?

El x86-64 SysV ABI especifica, entre otras cosas, cómo se pasan los parámetros de función en los registros (primer argumento enrdi, luegorsi y así sucesivamente), y cómo se devuelven los valores de retorno enteros (enrax y entoncesrdx para valores realmente grandes).

Sin embargo, lo que no puedo encontrar es cuáles deberían ser los bits altos de los parámetros o los registros de valor de retorno al pasar tipos menores de 64 bits.

Por ejemplo, para la siguiente función:

void foo(unsigned x, unsigned y);

...x será pasado enrdi yy enrsi, pero son solo 32 bits. Hacer los altos 32 bits derdi yrsi necesita ser cero? Intuitivamente, supongo que sí, pero elcódigo generado por todos los gcc, clang y icc tiene específicosmov instrucciones al principio para poner a cero los bits altos, por lo que parece que los compiladores suponen lo contrario.

Del mismo modo, los compiladores parecen suponer que los bits altos del valor de retornorax puede tener bits de basura si el valor de retorno es menor que 64 bits. Por ejemplo, los bucles en el siguiente código:

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;
}

...compilar a lo siguiente enclang (y otros compiladores son similares):

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

Nota lamov eax, eax después de la llamada que devuelve 32 bits, y elmovzx eax, ax después de la llamada de 16 bits, ambos tienen el efecto de poner a cero los 32 o 48 bits superiores, respectivamente. Por lo tanto, este comportamiento tiene algún costo: el mismo bucle que trata con un valor de retorno de 64 bits omite esta instrucción.

He leído elDocumento x86-64 System V ABI con mucho cuidado, pero no pude encontrar si este comportamiento está documentado en el estándar.

¿Cuáles son los beneficios de tal decisión? Me parece que hay costos claros:

Costos de parámetros

Los costos se imponen en la implementación de la persona que llama cuando se trata de valores de parámetros. y en las funciones cuando se trata con los parámetros. De acuerdo, a menudo este costo es cero porque la función puede ignorar efectivamente los bits altos, o la reducción a cero es gratuita ya que se pueden usar instrucciones de tamaño de operando de 32 bits que implícitamente ponen a cero los bits altos.

Sin embargo, los costos son a menudo muy reales en los casos de funciones que aceptan argumentos de 32 bits y hacen algunas matemáticas que podrían beneficiarse de las matemáticas de 64 bits. Tomaresta función por ejemplo:

uint32_t average(uint32_t a, uint32_t b) {
  return ((uint64_t)a + b) >> 2;
}

Un uso directo de las matemáticas de 64 bits para calcular una función que de otro modo tendría que tratar con cuidado el desbordamiento (la capacidad de transformar muchas funciones de 32 bits de esta manera es un beneficio a menudo inadvertido de las arquitecturas de 64 bits). Esto compila a:

average(unsigned int, unsigned int):
        mov     edi, edi
        mov     eax, esi
        add     rax, rdi
        shr     rax, 2
        ret  

Completamente 2 de las 4 instrucciones (ignorandoret) son necesarios para poner a cero los bits altos. Esto puede ser barato en la práctica con la eliminación de mov, pero aún así parece un gran costo a pagar.

Por otro lado, realmente no puedo ver un costo correspondiente similar para las personas que llaman si el ABI especificara que los bits altos son cero. Porquerdi yrsi y los otros registros de paso de parámetros sonrasguño (es decir, puede ser sobrescrito por la persona que llama), solo tiene un par de escenarios (miramosrdi, pero reemplácelo con el registro de parámetros de su elección):

El valor pasado a la función enrdi está muerto (no es necesario) en el código posterior a la llamada. En ese caso, cualquier instrucción asignada por última vez ardi simplemente tiene que asignar aedi en lugar. No solo es gratis, a menudo es un byte más pequeño si evita un prefijo REX.

El valor pasado a la función enrdi es necesario después de la función. En ese caso, desderdi se guarda la llamada, la persona que llama debe hacer unmov del valor a un registro guardado por el llamado de todos modos. En general, puede organizarlo para que el valorempieza en el registro guardado de la persona que llama (digamosrbx) y luego se mueve aedi me gustamov edi, ebx, entonces no cuesta nada.

No puedo ver muchos escenarios donde la reducción a cero le cuesta mucho a la persona que llama. Algunos ejemplos serían si se necesitan matemáticas de 64 bits en la última instrucción que asignórdi. Sin embargo, eso parece bastante raro.

Costos de valor de retorno

Aquí la decisión parece más neutral. Tener a los callees limpiando la basura tiene un código definido (a veces vesmov eax, eax instrucciones para hacer esto), pero si se permite la basura, los costos se trasladan a la persona que llama. En general, parece más probable que la persona que llama pueda eliminar la basura de forma gratuita, por lo que permitir que la basura no parezca perjudicial para el rendimiento.

Supongo que un caso de uso interesante para este comportamiento es que las funciones con diferentes tamaños pueden compartir una implementación idéntica. Por ejemplo, todas las siguientes funciones:

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;
}

Puede compartir la misma implementación1:

sum:
        lea     rax, [rdi+rsi]
        ret

1 Si tal plegamiento es realmentepermitido para las funciones que tienen su dirección tomada es muchoabierto a debate.