¿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ámetrosLos 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.
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.