TL;DR: muitas fontes citam a afirmação de que o uso excessivo de funções em linha pode, às vezes, prejudicar o desempenho do aplicativo devido ao excesso de código ou outros fatores. Existe algum exemplo real de programa que demonstre isso de forma mensurável?
Lembre-se: a missão de um microbenchmark na vida é ampliar algum aspecto do desempenho do seu programa. Por isso, qualquer pessoa pode facilmente gerar um microbenchmark que faça qualquer problema parecer um grande problema. // Dicas de Desempenho de Rico Mariani
Muitos programadores com quem converso têm a noção de que o inline de funções é incondicionalmente benéfico para o desempenho do aplicativo. O código C/C++ que analiso frequentemente tem inline
palavras-chave (ou equivalente) aplicadas gratuitamente a funções, independentemente de seu tamanho, finalidade, popularidade ou posicionamento.
Em muitos casos, esse hábito estranho (chamado aqui de "doença do inline" ) é inofensivo para o desempenho geral: os compiladores modernos têm bom senso sobre o que realmente deve ser incorporado, e muito pouco código é quente o suficiente para que o (não)inline faça alguma diferença. Ainda assim, muitas vezes é prejudicial ao design do software resultante: mais coisas acabam nos cabeçalhos, os arquivos não são mais compiláveis independentemente, etc.
Embora seja bastante fácil demonstrar que a aplicação aleatória sem benchmarking contínuo não faz nenhuma diferença mensurável no desempenho final, estou procurando um exemplo extremo em que forçar a questão prejudica estritamente o desempenho.
Um microbenchmark será suficiente; embora não prove nada sobre os efeitos do inline em aplicações do mundo real, deve demonstrar de forma comprovada que aplicá-lo cegamente não é uma boa ideia incondicional . Essa é realmente a ideia por trás de quase qualquer processo de otimização de código: pode ajudar, pode prejudicar e, às vezes, não faz diferença.
Alguns requisitos para tal exemplo de microbenchmark.
- Deve ser um programa razoavelmente curto, de preferência em C ou C++; outras linguagens nas quais o inlining pode ser aplicado também são bem-vindas.
- Não precisa ser um programa fazendo algo útil, ele pode fazer coisas "bobas" só para carregar/estressar o hardware subjacente.
- Deve ser possível compilá-lo em dois modos: com inlining imposto e inlining desabilitado. Qualquer técnica para conseguir isso pode ser usada: compilação condicional para redefinir anotações de inlining, sinalizadores de backend do compilador para controlar o inlining, etc.
- Ele deve ser bem formado e exibir o mesmo comportamento bem definido, independentemente de em qual dos dois modos ele for compilado.
- Ele deve conter pelo menos duas funções, uma chamando a outra, com a intenção de afetar o inlining de pelo menos uma delas.
- Pode conter qualquer técnica para garantir/impor a inclusão de funções em linha. Por exemplo,
inline
extensões padrão de palavras-chave ou específicas do compilador (__forceinline
,__attribute__ ((always_inline))
etc.) podem ser usadas para instruir o compilador a fazê-lo, independentemente de seu julgamento. - Ao executar, o desempenho (latência, tempo de execução ou métrica semelhante) pode ser facilmente reportado. Pode ser apenas usando
time a.out
, ou chamadas internas para um recurso de temporização em torno do código afetado. - Por fim, quando compilado por pelo menos um compilador específico de uma versão específica e executado em pelo menos um sistema de destino, as duas variantes resultantes do programa exibem diferenças estatisticamente significativas, e a compilação forçada em linha é mais lenta do que a compilação não em linha .
Eu percebo que o desempenho depende muito dos parâmetros do host; o que é mais lento em uma máquina pode se tornar tão rápido quanto ou mais rápido em outra. Mas estou buscando o pior cenário, quando o inlining irrestrito for comprovadamente contraproducente.
O ideal é que outras opções de backend do compilador que não afetam o inlining (como nível geral de otimização etc.) sejam as mesmas para duas compilações, a fim de excluir a possibilidade de que a diferença observável seja explicada por elas e não pelo inlining aplicado/ignorado.
Tenho uma ideia de um ponto de partida para esse programa, mas preciso de mais ajuda para desenvolvê-lo:
- Uma função interna é grande o suficiente para quase não caber no cache de instruções da CPU.
- Uma função externa é grande o suficiente para que, se a função interna for incorporada à força, a seção de código resultante se torne maior que o cache de instruções da CPU.
- O fluxo de controle do programa é organizado de tal forma que, quando tudo é incorporado, ele sofre uma frequência maior de falhas de cache de instruções, liberações de cache ou eventos semelhantes que não aconteceriam se o embutimento não fosse imposto.
Eu experimentei e usei o desenrolamento de loop do GCC para obter muito código de máquina a partir do seguinte código C:
Salvando isso
inlinecost.c
e compilando via:dando-me os seguintes binários:
Mostrando que certamente está gerando mais código.
Correr
inlinecost
me dá:enquanto
inlinecost-unrolled
me dá:Você pode ver que o código não embutido é executado de forma muito mais consistente, enquanto a versão desenrolada leva 10 vezes mais tempo para carregar o código de máquina da RAM para o cache e executá-lo, e então "apenas" leva o dobro do tempo para executá-lo.
Ter o loop
benchmark
gerando mais iterações (por exemplo, aumentando 5000 para 10000) torna essa diferença ainda mais visível, mas leva muito tempo para compilar.Aqui está um link para o GodBolt com apenas 5 iterações desenroladas (muitas iterações fazem com que a compilação atinja o tempo limite porque está gerando muito código), mostrando que ele está incorporando o PRNG.
Espero que seja útil!
Atualização: tentei mudar
benchmark
para fazer:Isso leva a versão desenrolada a ~400µs na primeira vez, depois ~50µs nas iterações subsequentes, enquanto a versão em loop parece levar de forma confiável ~7µs. Eu esperava que o preditor de ramificação tivesse dificuldades com tanto código, mas pelo menos minha CPU está se saindo notavelmente bem com isso — um AMD 9900X, ou seja, Zen5. Não sei por que me lembrei do Zen4 no meu comentário abaixo.