Estou tentando entender as diferenças entre um intérprete tradicional, um compilador JIT, um intérprete JIT e um compilador AOT.
Um intérprete é apenas uma máquina (virtual ou física) que executa instruções em alguma linguagem de computador. Nesse sentido, a JVM é um intérprete e as CPUs físicas são intérpretes.
Compilação antecipada significa simplesmente compilar o código em algum idioma antes de executá-lo (interpretá-lo).
No entanto, não tenho certeza sobre as definições exatas de um compilador JIT e um intérprete JIT.
De acordo com uma definição que li, a compilação JIT é simplesmente compilar o código imediatamente antes de interpretá-lo.
Então, basicamente, a compilação JIT é AOT, feita logo antes da execução (interpretação)?
E um intérprete JIT, é um programa que contém um compilador JIT e um intérprete e compila código (JITs) imediatamente antes de interpretá-lo?
Por favor, esclareça as diferenças.
fonte
Respostas:
visão global
Um intérprete de linguagem X é um programa (ou uma máquina, ou algum tipo de mecanismo de um modo geral), que executa qualquer programa p escrito em linguagem X de tal forma que ele executa os efeitos e avalia os resultados como prescrito pela especificação de X . As CPUs são geralmente intérpretes para seus respectivos conjuntos de instruções, embora as CPUs modernas de estação de trabalho de alto desempenho sejam realmente mais complexas que isso; eles podem realmente ter um conjunto de instruções privadas proprietárias subjacentes e converter (compilar) ou interpretar o conjunto de instruções públicas visíveis externamente.
Um compilador de X a Y é um programa (ou uma máquina, ou apenas algum tipo de mecanismo em geral) que traduz qualquer programa p de alguma linguagem X em um programa semanticamente equivalente p ' em alguma linguagem Y de tal maneira que a semântica do programa são conservados, isto é, que a interpretação p ' com um intérprete para Y irá produzir os mesmos resultados e têm os mesmos efeitos que a interpretação de p com um intérprete para X . (Observe que X e Y podem ser o mesmo idioma.)
Os termos AOT (Antecipação do Tempo) e Just-in-Time (JIT) se referem ao momento da compilação: o "tempo" referido nesses termos é "tempo de execução", ou seja, um compilador JIT compila o programa como está. Em execução , um compilador AOT compila o programa antes de ser executado . Observe que isso requer que um compilador JIT do idioma X para o idioma Y de alguma forma trabalhe em conjunto com um intérprete para o idioma Y, caso contrário, não haveria maneira de executar o programa. (Assim, por exemplo, um compilador JIT que compila JavaScript para código de máquina x86 não faz sentido sem uma CPU x86; ele compila o programa enquanto está em execução, mas sem a CPU x86 o programa não estaria em execução.)
Observe que essa distinção não faz sentido para intérpretes: um intérprete executa o programa. A ideia de um intérprete AOT que executa um programa antes da execução ou de um intérprete JIT que executa um programa enquanto está em execução não faz sentido.
Então nós temos:
Compiladores JIT
Dentro da família de compiladores JIT, ainda existem muitas diferenças sobre quando exatamente eles compilam, com que frequência e com que granularidade.
O compilador JIT no CLR da Microsoft, por exemplo, apenas compila o código uma vez (quando está carregado) e compila um assembly inteiro por vez. Outros compiladores podem coletar informações enquanto o programa está em execução e recompilar o código várias vezes à medida que novas informações se tornam disponíveis, o que lhes permite otimizar melhor. Alguns compiladores JIT são capazes de des otimizar o código. Agora, você pode se perguntar por que alguém iria querer fazer isso? A desotimização permite executar otimizações muito agressivas que podem ser realmente inseguras: se você for muito agressivo, poderá voltar atrás, enquanto que, com um compilador JIT que não pode des otimizar, você não poderá executar o otimizações agressivas em primeiro lugar.
Os compiladores JIT podem compilar alguma unidade estática de código de uma só vez (um módulo, uma classe, uma função, um método, ...; geralmente são chamados JIT de método por vez , por exemplo) ou podem rastrear a dinâmica execução de código para encontrar rastreamentos dinâmicos (normalmente loops) que eles compilarão (chamados de JITs de rastreamento ).
Combinando intérpretes e compiladores
Intérpretes e compiladores podem ser combinados em um mecanismo de execução de idioma único. Existem dois cenários típicos em que isso é feito.
Combinando um compilador AOT de X para Y com um intérprete para Y . Aqui, tipicamente X é uma linguagem de nível superior otimizada para facilitar a leitura por seres humanos, enquanto Yé uma linguagem compacta (geralmente algum tipo de bytecode) otimizada para interpretabilidade pelas máquinas. Por exemplo, o mecanismo de execução CPython Python possui um compilador AOT que compila o código-fonte Python no bytecode do CPython e um intérprete que interpreta o bytecode do CPython. Da mesma forma, o mecanismo de execução YARV Ruby possui um compilador AOT que compila o código-fonte Ruby no bytecode YARV e um intérprete que interpreta o bytecode YARV. Por que você gostaria de fazer isso? Ruby e Python são linguagens de nível muito alto e um tanto complexas; portanto, primeiro as compilamos em uma linguagem que é mais fácil de analisar e mais fácil de interpretar e depois interpretar essa linguagem.
A outra maneira de combinar um intérprete e um compilador é um mecanismo de execução em modo misto . Aqui, nós "mix" dois "modos" de implementar a mesma linguagem em conjunto, ou seja, um intérprete para X e um compilador JIT de X para Y . (Portanto, a diferença aqui é que, no caso acima, tivemos vários "estágios" com o compilador compilando o programa e, em seguida, alimentando o resultado no intérprete, aqui temos os dois trabalhando lado a lado no mesmo idioma. ) O código que foi compilado por um compilador tende a executar mais rapidamente do que o código executado por um intérprete, mas compilar o código primeiro leva tempo (e principalmente, se você deseja otimizar fortemente o código para execução)muito rápido, leva muito tempo). Portanto, para fazer uma ponte neste momento em que o compilador JIT está ocupado compilando o código, o intérprete já pode começar a executar o código e, assim que o JIT terminar de compilar, podemos alternar a execução para o código compilado. Isso significa que obtemos o melhor desempenho possível do código compilado, mas não precisamos esperar a compilação terminar, e nosso aplicativo começa a ser executado imediatamente (embora não seja o mais rápido possível).
Na verdade, essa é apenas a aplicação mais simples possível de um mecanismo de execução em modo misto. Possibilidades mais interessantes são, por exemplo, não começar a compilar imediatamente, mas permitir que o intérprete corra um pouco e colete estatísticas, informações de perfil, informações de tipo, informações sobre a probabilidade de ramificações condicionais específicas, quais métodos são chamados na maioria das vezes etc. e, em seguida, alimente essas informações dinâmicas ao compilador para que ele possa gerar código mais otimizado. Essa também é uma maneira de implementar a des otimização de que falei acima: se você for muito agressivo na otimização, poderá jogar fora (uma parte do) código e voltar a interpretar. A JVM do HotSpot faz isso, por exemplo. Ele contém um intérprete para o bytecode da JVM e um compilador para o bytecode da JVM. (De fato,dois compiladores!)
É também possível e, de facto, comum combinar estas duas abordagens: duas fases, com o primeiro sendo um compilador AOT que compila X a Y e a segunda fase sendo um motor de modo misto que tanto interpreta Y e compila Y a Z . O mecanismo de execução Rubinius Ruby funciona dessa maneira, por exemplo: ele possui um compilador AOT que compila o código-fonte Ruby para o bytecode do Rubinius e um mecanismo de modo misto que primeiro interpreta o bytecode do Rubinius e, depois de reunir algumas informações, compila os métodos mais frequentemente chamados no nativo Código da máquina.
Observe que o papel que o intérprete desempenha no caso de um mecanismo de execução em modo misto, ou seja, fornecer inicialização rápida e também coletar informações potencialmente e fornecer recurso de fallback, também pode ser desempenhado por um segundo compilador JIT. É assim que a V8 funciona, por exemplo. O V8 nunca interpreta, ele sempre compila. O primeiro compilador é um compilador muito rápido e muito fino que é iniciado muito rapidamente. O código que produz não é muito rápido, no entanto. Esse compilador também injeta código de criação de perfil no código que gera. O outro compilador é mais lento e usa mais memória, mas produz código muito mais rápido, e pode usar as informações de criação de perfil coletadas executando o código compilado pelo primeiro compilador.
fonte
python -m compileall .
ou carregar os módulos uma vez. Mesmo no último caso, como os arquivos permanecem e são reutilizados após a primeira execução, parece AOT.