Por que os compiladores não incorporam tudo? [fechadas]

12

À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?

Aviv Cohn
fonte
17
IDK sobre você, mas não tenho centenas de GB de memória por aí.
Ampt
2
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.
Doval 28/08
5
Você é excessivamente genérico, "compiladores" significa "todos os compiladores" e "tudo" realmente significa "tudo"? Então a resposta é simples, há situações em que você simplesmente não pode fazer a fila. A recursão vem à mente.
Otávio Décio
17
A localidade do cache é muito mais importante do que uma pequena sobrecarga de chamada de função.
SK-logic
3
Atualmente, a melhoria do desempenho realmente importa com centenas de GFLOPS de poder de processamento?
Mouviciel 28/08

Respostas:

22

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.

AProgrammer
fonte
3
algumas chamadas recursiva pode ser embutido (chamadas de cauda), mas tudo pode ser transformado em iteração se você adicionar, opcionalmente, um explícita pilha
aberração catraca
@ratchetfreak, você também pode transformar uma chamada recursiva não cauda em uma cauda. Mas isso é para mim no campo "difícil" (especialmente quando você tem funções co-recursivas ou precisa determinar dinamicamente para onde pular para simular o retorno), mas isso não é impossível (você apenas implementa uma estrutura de continuação e considerando que o presente se torna mais fácil).
APROGRAMMAR
11

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.

Simon Bergot
fonte
3
Inlining através de um despacho virtual é muito difícil, mas não impossível. Alguns compiladores C ++ são capazes de fazê-lo sob certas circunstâncias.
Bstamour 28/08/14
2
... bem como alguns compiladores JIT (desirtualização).
Frank
@bstamour Qualquer compilador semi-decente de qualquer idioma com otimizações apropriadas enviará estaticamente, ou seja, desvirtualizar, uma chamada para um método virtual declarado em um objeto cujo tipo dinâmico é conhecido no momento da compilação. Isso pode facilitar o inlining se a fase de desirtualização ocorrer antes da (ou outra) fase de inlining. Mas isso é trivial. Havia algo mais que você quis dizer? Não vejo como qualquer "Inlining através de um despacho virtual" real possa ser alcançado. Para in-line, é preciso saber o tipo estático - ou seja devirtualise - por isso a existência de meios Inlining não é nenhuma expedição virtual
underscore_d
9

Primeiro, nem sempre você pode alinhar, por exemplo, funções recursivas nem sempre podem ser embutidas (mas um programa contendo uma definição recursiva factcom apenas uma impressão de fact(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 CALLinstruçã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.

Basile Starynkevitch
fonte
1
Para o registro, o LLVM / Clang (e vários outros compiladores) também oferece suporte à otimização do tempo de link .
Você
Eu sei disso; O LTO existia no século anterior (IIRC, pelo menos em algum compilador proprietário do MIPS).
Basile Starynkevitch
7

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

Tom Tanner
fonte
este parece meramente pontos repetir feito e explicado em uma resposta antes que foi postado há uma hora
mosquito
1
Quais caches? L1? L2? L3? Qual deles é mais importante?
Peter Mortensen
1

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.

m3th0dman
fonte
1

Primeiro, há exemplos simples em que tudo indica que tudo funcionará muito mal. Considere este código C simples:

void f1 (void) { printf ("Hello, world\n"); }
void f2 (void) { f1 (); f1 (); f1 (); f1 (); }
void f3 (void) { f2 (); f2 (); f2 (); f2 (); }
...
void f99 (void) { f98 (); f98 (); f98 (); f98 (); }

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.

gnasher729
fonte