Às vezes, os compiladores chamam funções em linha. Isso significa que eles movem o código da função chamada para a função de chamada. Isso torna as coisas um pouco mais rápidas, porque não há necessidade de colocar e colocar coisas dentro e fora da pilha de chamadas.
Então, minha pergunta é: por que os compiladores não incorporam tudo? Suponho que tornaria o executável notavelmente mais rápido.
A única razão pela qual consigo pensar é em um executável significativamente maior, mas isso realmente importa atualmente com centenas de GB de memória? O desempenho aprimorado não vale a pena?
Existe alguma outra razão pela qual os compiladores não apenas incorporam todas as chamadas de função?
optimization
compiler
Aviv Cohn
fonte
fonte
Isn't the improved performance worth it?
Para um método que executa um loop 100 vezes e processa alguns números sérios, a sobrecarga de mover 2 ou 3 argumentos para os registros da CPU não é nada.Respostas:
Primeiro, observe que um dos principais efeitos do inline é que ele permite otimizações adicionais no site da chamada.
Para sua pergunta: existem coisas difíceis ou mesmo impossíveis de alinhar:
bibliotecas vinculadas dinamicamente
funções determinadas dinamicamente (despacho dinâmico, chamado por meio de ponteiros de função)
funções recursivas (pode recursão de cauda)
funções para as quais você não possui o código (mas a otimização do tempo do link permite isso para algumas delas)
Então inlining não tem apenas efeitos benéficos:
executável maior significa mais espaço em disco e maior tempo de carregamento
executável maior significa aumento da pressão do cache (observe que incluir funções suficientemente pequenas, como getters simples, pode diminuir o tamanho do executável e a pressão do cache)
E, finalmente, para funções que levam um tempo não trivial para executar, o ganho simplesmente não vale a pena.
fonte
Uma grande limitação é o polimorfismo em tempo de execução. Se houver um despacho dinâmico acontecendo quando você escreve
foo.bar()
, é impossível alinhar a chamada do método. Isso explica por que os compiladores não incorporam tudo.As chamadas recursivas também não podem ser facilmente incorporadas.
A inserção cruzada de módulos também é difícil de executar por razões técnicas (a recompilação incremental seria impossível para ex)
No entanto, os compiladores incorporam muitas coisas.
fonte
Primeiro, nem sempre você pode alinhar, por exemplo, funções recursivas nem sempre podem ser embutidas (mas um programa contendo uma definição recursiva
fact
com apenas uma impressão defact(8)
pode ser embutido).Então, inlining nem sempre é benéfico. Se o compilador incluir tanto que o código de resultado seja grande o suficiente para que suas partes quentes não se ajustem, por exemplo, ao cache de instruções L1, ele poderá ser muito mais lento que a versão não embutida (que caberia facilmente no cache L1) ... Além disso, os processadores recentes são muito rápidos na execução de uma
CALL
instrução da máquina (pelo menos em um local conhecido, ou seja, uma chamada direta, não uma chamada através do ponteiro).Por fim, o alinhamento completo requer uma análise completa do programa. Isso pode não ser possível (ou é muito caro). Com o C ou C ++ compilado pelo GCC (e também com o Clang / LLVM ), você precisa habilitar a otimização do tempo de link (compilando e vinculando com, por exemplo
g++ -flto -O2
) e isso leva bastante tempo de compilação.fonte
Por mais surpreendente que pareça, incluir tudo não reduz necessariamente o tempo de execução. O tamanho aumentado do seu código pode dificultar que a CPU mantenha todo o seu código no cache de uma só vez. Uma falta de cache no seu código se torna mais provável e uma falta de cache é cara. Isso fica muito pior se as funções potencialmente incorporadas forem grandes.
De tempos em tempos, eu tenho notado aprimoramentos visíveis ao retirar grandes blocos de código marcados como 'inline' dos arquivos de cabeçalho e colocá-los no código-fonte, para que o código esteja apenas em um local e não em todos os sites de chamada. Em seguida, o cache da CPU é melhor utilizado e você também obtém melhor tempo de compilação ...
fonte
Incluir tudo não significaria apenas aumento no consumo de memória em disco, mas também aumento no consumo de memória interna, o que não é tão abundante. Lembre-se de que o código também depende da memória no segmento de código; se uma função é chamada de 10.000 locais (digamos os de bibliotecas padrão em um projeto bastante grande), o código dessa função ocupa 10.000 vezes mais memória interna.
Outro motivo pode ser os compiladores JIT; se tudo estiver em linha, não haverá pontos de acesso a serem compilados dinamicamente.
fonte
Primeiro, há exemplos simples em que tudo indica que tudo funcionará muito mal. Considere este código C simples:
Adivinhe o que tudo isso fará com você.
Em seguida, você assume que o inlining tornará as coisas mais rápidas. Às vezes é esse o caso, mas nem sempre. Uma razão é que o código que se encaixa no cache de instruções é executado muito mais rápido. Se eu chamar uma função de 10 lugares, sempre executarei o código que está no cache de instruções. Se estiver embutido, as cópias estão em todo o lugar e ficam muito mais lentas.
Existem outros problemas: o embutimento produz enormes funções. Funções enormes são muito mais difíceis de otimizar. Eu tenho ganhos consideráveis no código crítico de desempenho, ocultando funções em um arquivo separado para impedir que o compilador as inclua. Como resultado, o código gerado para essas funções era muito melhor quando elas estavam ocultas.
Entre. Eu não tenho "centenas de GBs de memória". Meu computador de trabalho nem tem "centenas de GBs de espaço no disco rígido". E se meu aplicativo contiver "centenas de GBs de memória", levará 20 minutos apenas para carregar o aplicativo na memória.
fonte