É necessário um sinal ou extensão zero ao adicionar um deslocamento de 32 bits a um ponteiro para o ABI x86-64?

Resumo: Eu estava olhando o código assembly para orientar minhas otimizações e ver muitas extensões de sinal ou zero ao adicionar int32 a um ponteiro.

void Test(int *out, int offset) { out[offset] = 1; } ------------------------------------- movslq %esi, %rsi movl $1, (%rdi,%rsi,4) ret 

No começo, achei que meu compilador foi desafiado a adicionar inteiros de 32 bits a 64 bits, mas eu confirmei esse comportamento com o Intel ICC 11, ICC 14 e GCC 5.3.

Este tópico confirma minhas descobertas, mas não está claro se o sinal ou extensão zero é necessário. Este sinal / extensão zero só seria necessário se os 32bits superiores ainda não estivessem definidos. Mas a ABI x86-64 não seria inteligente o suficiente para exigir isso?

Eu estou meio relutante em mudar todos os meus deslocamentos de ponteiro para ssize_t porque o registro de spills aumentará o footprint do cache do código.

Sim, você deve assumir que os 32 bits altos de um registrador arg ou return-value contêm lixo. Você está autorizado a deixar o lixo no alto 32 ao ligar ou retornar a si mesmo, no entanto.

Você precisa assinar ou estender até 64 bits para usar o valor em um endereço efetivo de 64 bits. Na ABI x32 , o gcc freqüentemente usa endereços efetivos de 32 bits em vez de usar o tamanho de operando de 64 bits para cada instrução que modifica um número inteiro potencialmente negativo usado como um índice de matriz.


O padrão:

O x86-64 SysV ABI apenas diz qualquer coisa sobre quais partes de um registro são zeradas para _Bool (também conhecido como bool ). Página 20:

Quando um valor do tipo _Bool é retornado ou passado em um registrador ou na pilha, o bit 0 contém o valor verdade e os bits 1 a 7 devem ser zero (nota 14: Outros bits não são especificados, portanto o lado do consumidor desses valores pode depende de ser 0 ou 1 quando truncado para 8 bits)

Além disso, o material sobre %al mantendo o número de argumentos de registro FP para funções varargs, não o %rax inteiro.

Há uma questão aberta sobre o github sobre esta pergunta exata na página do github para os documentos ABI x32 e x86-64 .

A ABI não coloca quaisquer requisitos ou garantias adicionais sobre o conteúdo das partes altas de registradores inteiros ou vetoriais que contêm argumentos ou valores de retorno, portanto, não há nenhum. Eu tenho confirmação deste fato via e-mail de Michael Matz (um dos mantenedores da ABI): “Geralmente, se a ABI não disser que algo está especificado, você não pode confiar nela.”

Ele também confirmou que, por exemplo, o uso de um addps clang> = 3.6 que poderia desacelerar ou levantar exceções extra de FP com o lixo em elementos altos é um bug (o que me lembra que eu devo relatar isso). Ele acrescenta que este foi um problema uma vez com uma implementação AMD de uma function matemática glibc. O código C normal pode deixar lixo em elementos altos de regs vetoriais ao passar argumentos escalares double ou float .


Comportamento real que não está (ainda) documentado no padrão:

Argumentos de function estreita, mesmo _Bool / bool , são sinal ou estendido de zero a 32 bits. clang até faz código que depende desse comportamento (desde 2007, aparentemente) . ICC17 não faz isso , então ICC e clang não são compatíveis com ABI , mesmo para C. Não chame funções compiladas de código compilado pela ICC para o x86-64 SysV ABI, se qualquer um dos primeiros 6 argumentos inteiros são mais estreitos que 32 bits.

Isso não se aplica aos valores de retorno, apenas args: gcc e clang assumem que os valores de retorno que recebem recebem dados válidos até a largura do tipo. O gcc fará funções retornando char que deixam lixo nos 24 bits de %eax , por exemplo.

Um tópico recente no grupo de discussão da ABI foi uma proposta para esclarecer as regras para estender args de 8 e 16 bits para 32 bits, e talvez realmente modificar a ABI para exigir isso. Os principais compiladores (exceto ICC) já fazem isso, mas seria uma mudança no contrato entre chamadores e callees.

Aqui está um exemplo (confira com outros compiladores ou ajuste o código no Godbolt Compiler Explorer , onde incluí muitos exemplos simples que demonstram apenas uma parte do quebra-cabeça, assim como isso demonstra muito):

 extern short fshort(short a); extern unsigned fuint(unsigned int a); extern unsigned short array_us[]; unsigned short lookupu(unsigned short a) { unsigned int a_int = a + 1234; a_int += fshort(a); // NOTE: not the same calls as the signed lookup return array_us[a + fuint(a_int)]; } # clang-3.8 -O3 for x86-64. arg in %rdi. (Actually in %di, zero-extended to %edi by our caller) lookupu(unsigned short): pushq %rbx # save a call-preserved reg for out own use. (Also aligns the stack for another call) movl %edi, %ebx # If we didn't assume our arg was already zero-extended, this would be a movzwl (aka movzx) movswl %bx, %edi # sign-extend to call a function that takes signed short instead of unsigned short. callq fshort(short) cwtl # Don't trust the upper bits of the return value. (This is cdqe, Intel syntax. eax = sign_extend(ax)) leal 1234(%rbx,%rax), %edi # this is the point where we'd get a wrong answer if our arg wasn't zero-extended. gcc doesn't assume this, but clang does. callq fuint(unsigned int) addl %ebx, %eax # zero-extends eax to 64bits movzwl array_us(%rax,%rax), %eax # This zero-extension (instead of just writing ax) is *not* for correctness, just for performance: avoid partial-register slowdowns if the caller reads eax popq %rbx retq 

Nota: movzwl array_us(,%rax,2) seria equivalente, mas não menor. Se pudéssemos depender dos altos bits de %rax sendo zerados no valor de retorno de fuint() , o compilador poderia ter usado array_us(%rbx, %rax, 2) vez de usar o add insn.


Implicações no desempenho

Deixar o high32 indefinido é intencional, e acho que é uma boa decisão de design.

Ignorando o alto 32 é livre ao fazer operações de 32 bits. Uma operação de 32 bits estende zero seu resultado para 64 bits gratuitamente , então você só precisa de um extra mov edx, edi ou algo assim se você pudesse ter usado o reg diretamente em um modo de endereçamento de 64 bits ou operação de 64 bits.

Algumas funções não salvam quaisquer insns de ter seus argumentos já estendidos para 64 bits, portanto, é um desperdício potencial para os chamadores sempre ter que fazer isso. Algumas funções usam seus argumentos de uma maneira que requer a extensão oposta da assinatura do argumento, deixando assim para o receptor decidir o que fazer.

No entanto, a extensão de zero para 64 bits, independentemente da assinatura, seria gratuita para a maioria dos chamadores e poderia ter sido uma boa escolha de design da ABI. Como arg regs são burlados de qualquer maneira, o chamador já precisa fazer algo extra se quiser manter um valor de 64 bits completo em uma chamada onde passa apenas o valor baixo 32. Assim, normalmente custa apenas mais quando você precisa de um de 64 bits. resultado para algo antes da chamada, e depois passar uma versão truncada para uma function. Em x86-64 SysV, você pode gerar seu resultado em RDI e usá-lo e, em seguida, call foo que só vai olhar para EDI.

Os tamanhos de operando de 16 bits e 8 bits geralmente levam a falsas dependencies (AMD, P4 ou Silvermont e posteriormente à família SnB) ou a retenções de registro parcial (pré SnB) ou pequenas lentidões (Sandybridge), portanto o comportamento não documentado de exigir que os tipos 8 e 16b sejam estendidos para 32b para passagem de arg faz algum sentido. Veja Por que o GCC não usa registros parciais? para mais detalhes sobre essas microarquiteturas.


Isso provavelmente não é grande coisa para o tamanho do código em código real, já que funções minúsculas são / devem ser static inline , e insns de manipulação de argumentos são uma pequena parte de funções maiores . Otimização inter-processual pode remover a sobrecarga entre as chamadas quando o compilador pode ver ambas as definições, mesmo sem inlining. (IDK como os compiladores fazem isso na prática.)

Não tenho certeza se a alteração de assinaturas de function para usar uintptr_t ajudará ou prejudicará o desempenho geral com pointers de 64 bits. Eu não me preocuparia com o espaço de pilha para escalares. Na maioria das funções, o compilador envia / %rbx registros preservados de chamadas (como %rbx e %rbp ) para manter suas próprias variables ​​ao vivo nos registradores. Um pequeno espaço extra para derrames de 8B em vez de 4B é insignificante.

Quanto ao tamanho do código, trabalhar com valores de 64 bits requer um prefixo REX em alguns inss que, de outra forma, não precisariam de um. A extensão zero para 64 bits ocorre de graça se qualquer operação for necessária em um valor de 32 bits antes de ser usada como um índice de matriz. A extensão de sinal sempre leva uma instrução extra se for necessária. Mas os compiladores podem estender a assinatura e trabalhar com ela como um valor assinado de 64 bits desde o início para salvar instruções, ao custo de precisar de mais prefixos REX. (O overflow assinado é UB, não definido para ser agrupado, portanto, os compiladores geralmente podem evitar refazer a extensão de sinal dentro de um loop com um int i que usa arr[i] .)

As CPUs modernas geralmente se importam mais com a contagem de inson do que com o tamanho insn, dentro da razão. O código de access normalmente estará sendo executado a partir do cache uop em CPUs que os possuem. Ainda assim, código menor pode melhorar a densidade no cache uop. Se você pode salvar o tamanho do código sem usar mais ou insins mais lentos, então é uma vitória, mas geralmente não vale a pena sacrificar qualquer outra coisa, a menos que tenha muito tamanho de código.

Como talvez uma instrução LEA extra para permitir que [reg + disp8] atenda a uma dúzia de instruções posteriores, em vez de disp32 . Ou xor eax,eax antes de várias mov [rdi+n], 0 instruções para replace o imm32 = 0 com uma fonte de registro. (Especialmente se isso permitir a microfusão, onde não seria possível com um parente RIP + imediato, porque o que realmente importa é a contagem de front-end, não a contagem de instruções.)

Como o comentário de EOF indica que o compilador não pode assumir que os 32 bits superiores de um registrador de 64 bits usado para passar um argumento de 32 bits tenham algum valor particular. Isso faz com que o sinal ou extensão zero seja necessário.

A única maneira de evitar isso seria usar um tipo de 64 bits para o argumento, mas isso move o requisito para estender o valor para o chamador, o que pode não ser uma melhoria. Eu não me preocuparia muito com o tamanho dos registros, já que a maneira como você está fazendo agora é provavelmente mais provável que após a extensão o valor original esteja morto e seja o valor estendido de 64 bits que será derramado . Mesmo se não estiver morto, o compilador ainda pode preferir derramar o valor de 64 bits.

Se você está realmente preocupado com a sua pegada de memory e não precisa do maior espaço de endereço de 64 bits, você pode olhar para a ABI x32 que usa os tipos ILP32, mas suporta o conjunto completo de instruções de 64 bits.

Intereting Posts