TL: DR:
- Os componentes internos do compilador provavelmente não estão configurados para procurar essa otimização facilmente, e provavelmente são úteis apenas em pequenas funções, não em grandes funções entre chamadas.
- Inlining para criar grandes funções é uma solução melhor na maioria das vezes
- Pode haver uma troca de latência x taxa de transferência se
foo
não salvar / restaurar o RBX.
Compiladores são peças complexas de máquinas. Eles não são "inteligentes" como humanos, e algoritmos caros para encontrar todas as otimizações possíveis geralmente não valem o custo em tempo de compilação extra.
Eu relatei isso como bug 69986 do GCC - código menor possível com -Os usando push / pop para vazar / recarregar em 2016 ; não houve atividade ou resposta dos desenvolvedores do GCC. : /
Ligeiramente relacionado: o bug 70408 do GCC - reutilizar o mesmo registro preservado de chamadas daria um código menor em alguns casos - os desenvolvedores do compilador me disseram que seria necessário muito trabalho para que o GCC pudesse fazer essa otimização porque requer ordem de seleção de duas foo(int)
chamadas com base no que tornaria o destino mais simples.
Se foo
não se salvar / restaurar rbx
, há uma troca entre taxa de transferência (contagem de instruções) e uma latência extra de armazenamento / recarga na x
cadeia de dependência -> retval.
Os compiladores geralmente favorecem a latência sobre a taxa de transferência, por exemplo, usando 2x LEA em vez de imul reg, reg, 10
(latência de 3 ciclos, taxa de transferência de 1 / clock), porque a maioria dos códigos calcula a média significativamente menor que 4 uops / clock em tubulações típicas de 4 larguras como Skylake. (Mais instruções / uops ocupam mais espaço no ROB, reduzindo o quão à frente a mesma janela fora de ordem pode ver, porém, e a execução é realmente cheia de barracas, provavelmente representando alguns dos menos de 4 uops / média do relógio.)
Se foo
for push / pop RBX, não há muito a ganhar em latência. O fato de a restauração ocorrer logo antes do em ret
vez de logo após provavelmente não é relevante, a menos que haja uma ret
falha de previsão incorreta ou de cache em I que atrasa a busca de código no endereço de retorno.
A maioria das funções não triviais salvará / restaurará o RBX, portanto, muitas vezes não é uma boa suposição que deixar uma variável no RBX signifique que ele realmente permaneceu em um registro durante a chamada. (Embora a escolha aleatória de quais funções de registradores preservados de chamada escolhem pode às vezes ser uma boa idéia para mitigar isso)
Portanto, sim push rdi
/ pop rax
seria mais eficiente nesse caso, e provavelmente essa é uma otimização perdida para pequenas funções que não são folhas, dependendo do que foo
faz e do equilíbrio entre a latência extra de armazenamento / recarga x
e mais instruções para salvar / restaurar o chamador rbx
.
É possível que os metadados de desenrolamento de pilha representem as alterações no RSP aqui, como se tivessem usado sub rsp, 8
para derramar / recarregar x
em um slot de pilha. (Mas compiladores não sei essa otimização, quer, de utilizar push
o espaço de reserva e inicializar uma variável. O que C / C ++ compilador pode usar instruções impulso pop para a criação de variáveis locais, em vez de apenas aumentar esp uma vez? . E fazendo isso por mais de um var local levaria a .eh_frame
metadados de desenrolamento de pilha maiores, porque você está movendo o ponteiro de pilha separadamente a cada envio. Isso não impede que os compiladores usem push / pop para salvar / restaurar registros preservados de chamada.)
IDK se valeria a pena ensinar aos compiladores a procurar essa otimização
Talvez seja uma boa idéia em torno de uma função inteira, não em uma chamada dentro de uma função. E como eu disse, é baseado na suposição pessimista de que foo
você salvará / restaurará o RBX de qualquer maneira. (Ou otimizar a taxa de transferência se você souber que a latência de x para retornar valor não é importante. Mas os compiladores não sabem disso e geralmente otimizam para latência).
Se você começar a fazer essa suposição pessimista em muitos códigos (como em torno de chamadas de função única dentro de funções), começará a receber mais casos em que o RBX não é salvo / restaurado e você poderia ter aproveitado.
Você também não deseja salvar / restaurar extra push / pop em um loop, apenas salve / restaure o RBX fora do loop e use registros preservados de chamadas em loops que fazem chamadas de função. Mesmo sem loops, no caso geral, a maioria das funções faz várias chamadas de função. Essa idéia de otimização pode ser aplicada se você realmente não usar x
entre nenhuma das chamadas, imediatamente antes da primeira e após a última, caso contrário , você terá um problema em manter o alinhamento da pilha de 16 bytes para cada uma, call
se estiver fazendo um pop após um antes de outra chamada.
Compiladores não são bons em pequenas funções em geral. Mas também não é ótimo para CPUs. As chamadas de função não em linha afetam a otimização o melhor dos momentos, a menos que os compiladores possam ver as partes internas do chamado e fazer mais suposições do que o habitual. Uma chamada de função não embutida é uma barreira implícita à memória: o chamador deve assumir que uma função pode ler ou gravar qualquer dado acessível globalmente, para que todos esses vars tenham que estar sincronizados com a máquina abstrata C. (A análise de escape permite manter os habitantes locais em registros nas chamadas, se o endereço deles não tiver escapado da função.) Além disso, o compilador deve assumir que os registros com excesso de chamada estão com excesso. Isso é péssimo para o ponto flutuante no x86-64 System V, que não possui registros XMM preservados por chamada.
Funções minúsculas, como bar()
é melhor incluir os chamadores. Compile -flto
para que isso possa acontecer mesmo dentro dos limites do arquivo na maioria dos casos. (Ponteiros de função e limites de biblioteca compartilhada podem anular isso.)
Acho que um dos motivos pelos quais os compiladores não se deram ao trabalho de tentar fazer essas otimizações é que isso exigiria um monte de código diferente nas partes internas do compilador , diferente da pilha normal e do código de alocação de registro que sabe como salvar chamadas preservadas registradores e usá-los.
ou seja, seria muito trabalho para implementar e muito código para manter, e se ficar entusiasmado demais com isso, poderá piorar o código.
E também que (espero) não é significativo; Se é importante, você deve ser inlining bar
em seu chamador, ou inlining foo
em bar
. Isso é bom, a menos que haja muitas bar
funções semelhantes e foo
seja grande e , por algum motivo, elas não possam ser incorporadas aos chamadores.