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âmetros

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

Custos com valor de retorno

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.

questionAnswers(1)

yourAnswerToTheQuestion