O lixo é permitido em bits altos de registradores de parâmetro e valor de retorno na ABI do SysV x86-64?
A ABI do x86-64 SysV especifica, entre outras coisas, como os parâmetros de função são passados nos registradores (o primeiro argumento emrdi
, entãorsi
e assim por diante) e como os valores de retorno inteiro são transmitidos de volta (emrax
e depoisrdx
por valores realmente grandes).
O que não consigo encontrar, no entanto, é o que devem ser os altos bits de parâmetro ou o valor de retorno ao passar tipos menores que 64 bits.
Por exemplo, para a seguinte função:
void foo(unsigned x, unsigned y);
...x
será passado emrdi
ey
norsi
, mas eles são apenas 32 bits. Faça os altos 32 bits derdi
ersi
precisa ser zero? Intuitivamente, eu assumiria que sim, mas ocódigo gerado por todos os gcc, clang e icc tem específicomov
instruções no início para zerar os bits altos, então parece que os compiladores assumem o contrário.
Da mesma forma, os compiladores parecem assumir que os bits altos do valor de retornorax
pode ter bits de lixo se o valor de retorno for menor que 64 bits. Por exemplo, os loops no seguinte 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 para o seguinte emclang
(e outros compiladores são semelhantes):
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
Note omov eax, eax
após a chamada retornar 32 bits, e omovzx eax, ax
após a chamada de 16 bits - ambos têm o efeito de zerar os 32 ou 48 bits principais, respectivamente. Portanto, esse comportamento tem algum custo - o mesmo loop que lida com um valor de retorno de 64 bits omite esta instrução.
Eu li oDocumento ABI do System V x86-64 com muito cuidado, mas não consegui descobrir se esse comportamento está documentado no padrão.
Quais são os benefícios de tal decisão? Parece-me que há custos claros:
Custos dos parâmetrosOs custos são impostos à implementação do chamado ao lidar com valores de parâmetros. e nas funções ao lidar com os parâmetros. É verdade que esse custo é zero porque a função pode efetivamente ignorar os bits altos ou o zeramento é gratuito, pois podem ser usadas instruções de tamanho de operando de 32 bits que zeram implicitamente os bits altos.
No entanto, os custos costumam ser muito reais nos casos de funções que aceitam argumentos de 32 bits e fazem algumas contas que poderiam se beneficiar da matemática de 64 bits. Tomaesta função por exemplo:
uint32_t average(uint32_t a, uint32_t b) {
return ((uint64_t)a + b) >> 2;
}
Um uso direto da matemática de 64 bits para calcular uma função que, de outra forma, precisaria lidar com excesso de capacidade (a capacidade de transformar muitas funções de 32 bits dessa maneira é um benefício muitas vezes despercebido das arquiteturas de 64 bits). Isso compila para:
average(unsigned int, unsigned int):
mov edi, edi
mov eax, esi
add rax, rdi
shr rax, 2
ret
Totalmente 2 das 4 instruções (ignorandoret
) são necessários apenas para zerar os bits altos. Na prática, isso pode ser barato com a eliminação de movimentos, mas ainda parece um grande custo a pagar.
Por outro lado, não vejo realmente um custo correspondente semelhante para os chamadores se a ABI especificar que os bits altos são zero. Porquerdi
ersi
e os outros registros de passagem de parâmetros sãocoçar, arranhão (ou seja, pode ser sobrescrito pelo chamador), você tem apenas alguns cenários (analisamosrdi
, mas substitua-o pelo parâmetro paramter de sua escolha):
O valor passado para a função emrdi
está morto (não é necessário) no código de pós-chamada. Nesse caso, qualquer instrução atribuída pela última vez ardi
simplesmente tem que atribuir aedi
em vez de. Isso não é apenas gratuito, como também é um byte menor se você evitar um prefixo REX.
O valor passado para a função emrdi
é necessário após a função. Nesse caso, desderdi
é salvo pelo chamador, ele precisa fazer umamov
do valor para um registro salvo por chamada de qualquer maneira. Geralmente, você pode organizá-lo para que o valorcomeça no registro salvo chamado (digamosrbx
) e depois é movido paraedi
gostarmov edi, ebx
, por isso não custa nada.
Não vejo muitos cenários em que o zeramento custa muito ao chamador. Alguns exemplos seriam se a matemática de 64 bits fosse necessária na última instrução que atribuiurdi
. Isso parece bastante raro.
Aqui a decisão parece mais neutra. Ter os calandres limpos o lixo eletrônico tem um código definido (às vezes você vêmov eax, eax
instruções para fazer isso), mas se o lixo for permitido, os custos mudam para o chamado. No geral, parece mais provável que o chamador possa limpar o lixo gratuitamente, portanto, permitir que o lixo não pareça prejudicial ao desempenho.
Suponho que um caso de uso interessante para esse comportamento é que funções com tamanhos variados podem compartilhar uma implementação idêntica. Por exemplo, todas as seguintes funções:
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;
}
Pode realmente compartilhar a mesma implementação1:
sum:
lea rax, [rdi+rsi]
ret
1 Se essa dobra é realmentepermitido para funções cujo endereço foi utilizado é muitoaberto ao debate.