Compilação just-in-time - Just-in-time compilation

Na computação , a compilação just-in-time ( JIT ) (também tradução dinâmica ou compilações em tempo de execução) é uma maneira de executar código de computador que envolve a compilação durante a execução de um programa (em tempo de execução ), e não antes da execução. Isso pode consistir na tradução do código-fonte, mas é mais comumente tradução de bytecode para código de máquina , que é então executado diretamente. Um sistema que implementa um compilador JIT normalmente analisa continuamente o código que está sendo executado e identifica as partes do código onde o aumento de velocidade obtido com a compilação ou recompilação superaria a sobrecarga de compilar esse código.

A compilação JIT é uma combinação das duas abordagens tradicionais de tradução para código de máquina - compilação antecipada (AOT) e interpretação - e combina algumas vantagens e desvantagens de ambas. Grosso modo, a compilação JIT combina a velocidade do código compilado com a flexibilidade de interpretação, com a sobrecarga de um interpretador e a sobrecarga adicional de compilar e vincular (não apenas interpretar). A compilação JIT é uma forma de compilação dinâmica e permite a otimização adaptativa , como recompilação dinâmica e acelerações específicas da microarquitetura . A interpretação e a compilação JIT são particularmente adequadas para linguagens de programação dinâmicas , pois o sistema de tempo de execução pode lidar com tipos de dados de ligação tardia e impor garantias de segurança.

História

O mais antigo compilador JIT publicado é geralmente atribuído ao trabalho em LISP por John McCarthy em 1960. Em seu artigo seminal Funções recursivas de expressões simbólicas e sua computação por máquina, Parte I , ele menciona funções que são traduzidas durante o tempo de execução, dispensando assim a necessidade de salve a saída do compilador em cartões perfurados (embora isso seja mais precisamente conhecido como " Compile and go system "). Outro exemplo inicial foi de Ken Thompson , que em 1968 deu uma das primeiras aplicações de expressões regulares , aqui para correspondência de padrões no editor de texto QED . Para velocidade, Thompson implementou a correspondência de expressão regular por JITing para o código IBM 7094 no Compatible Time-Sharing System . Uma técnica influente para derivar código compilado da interpretação foi iniciada por James G. Mitchell em 1970, que ele implementou para a linguagem experimental LC² .

Smalltalk (c. 1983) foi o pioneiro em novos aspectos das compilações JIT. Por exemplo, a tradução para o código de máquina foi feita sob demanda e o resultado foi armazenado em cache para uso posterior. Quando a memória se tornava escassa, o sistema excluía parte desse código e o regenerava quando fosse necessário novamente. A linguagem Self da Sun melhorou extensivamente essas técnicas e chegou a ser o sistema Smalltalk mais rápido do mundo; alcançando até a metade da velocidade do C otimizado, mas com uma linguagem totalmente orientada a objetos.

Self foi abandonado pela Sun, mas a pesquisa foi para a linguagem Java. O termo "compilação Just-in-time" foi emprestado do termo de manufatura " Just in time " e popularizado por Java, com James Gosling usando o termo de 1993. Atualmente JITing é usado pela maioria das implementações da Java Virtual Machine , como HotSpot baseia-se e usa extensivamente essa base de pesquisa.

O projeto HP Dynamo era um compilador JIT experimental onde o formato 'bytecode' e o formato do código de máquina eram os mesmos; o sistema transformou o código de máquina PA-6000 em código de máquina PA-8000 . Contra-intuitivamente, isso resultou em acelerações, em alguns casos de 30%, uma vez que isso permitiu otimizações no nível do código de máquina, por exemplo, código embutido para melhor uso de cache e otimizações de chamadas para bibliotecas dinâmicas e muitas outras otimizações de tempo de execução convencionais os compiladores não podem tentar.

Em novembro de 2020, o PHP 8.0 introduziu um compilador JIT.

Projeto

Em um sistema compilado por bytecode, o código-fonte é traduzido para uma representação intermediária conhecida como bytecode . Bytecode não é o código de máquina de qualquer computador em particular e pode ser portátil entre arquiteturas de computador. O bytecode pode então ser interpretado ou executado em uma máquina virtual . O compilador JIT lê os bytecodes em muitas seções (ou na íntegra, raramente) e os compila dinamicamente em código de máquina para que o programa possa ser executado mais rapidamente. Isso pode ser feito por arquivo, por função ou mesmo em qualquer fragmento de código arbitrário; o código pode ser compilado quando está prestes a ser executado (daí o nome "just-in-time") e, em seguida, armazenado em cache e reutilizado posteriormente sem a necessidade de recompilação.

Em contraste, uma máquina virtual interpretada tradicional simplesmente interpretará o bytecode, geralmente com desempenho muito inferior. Alguns intérpretes até interpretam o código-fonte, sem a etapa de compilar primeiro para o bytecode, com desempenho ainda pior. O código compilado estaticamente ou o código nativo é compilado antes da implantação. Um ambiente de compilação dinâmica é aquele em que o compilador pode ser usado durante a execução. Um objetivo comum de usar técnicas JIT é alcançar ou superar o desempenho da compilação estática , enquanto mantém as vantagens da interpretação do bytecode: muito do "trabalho pesado" de analisar o código-fonte original e realizar a otimização básica é frequentemente tratado no tempo de compilação, antes da implantação: a compilação de bytecode para código de máquina é muito mais rápida do que compilar a partir da fonte. O bytecode implantado é portátil, ao contrário do código nativo. Como o tempo de execução tem controle sobre a compilação, como o bytecode interpretado, ele pode ser executado em uma caixa de proteção segura. Compiladores de bytecode para código de máquina são mais fáceis de escrever, porque o compilador de bytecode portátil já fez grande parte do trabalho.

O código JIT geralmente oferece um desempenho muito melhor do que os intérpretes. Além disso, pode, em alguns casos, oferecer melhor desempenho do que a compilação estática, pois muitas otimizações só são viáveis ​​em tempo de execução:

  1. A compilação pode ser otimizada para a CPU de destino e o modelo de sistema operacional onde o aplicativo é executado. Por exemplo, o JIT pode escolher as instruções da CPU do vetor SSE2 quando detecta que a CPU as suporta. Para obter esse nível de especificidade de otimização com um compilador estático, deve-se compilar um binário para cada plataforma / arquitetura pretendida ou incluir várias versões de partes do código em um único binário.
  2. O sistema é capaz de coletar estatísticas sobre como o programa está realmente sendo executado no ambiente em que se encontra e pode reorganizar e recompilar para obter um desempenho ideal. No entanto, alguns compiladores estáticos também podem receber informações de perfil como entrada.
  3. O sistema pode fazer otimizações globais de código (por exemplo, inlining de funções de biblioteca) sem perder as vantagens da vinculação dinâmica e sem as sobrecargas inerentes aos compiladores e vinculadores estáticos. Especificamente, ao fazer substituições globais em linha, um processo de compilação estática pode precisar de verificações em tempo de execução e garantir que uma chamada virtual ocorra se a classe real do objeto substituir o método embutido, e verificações de condição de limite em acessos de array podem precisar ser processadas dentro de loops. Com a compilação just-in-time, em muitos casos, esse processamento pode ser movido para fora dos loops, geralmente proporcionando grandes aumentos de velocidade.
  4. Embora isso seja possível com linguagens coletadas de lixo compiladas estaticamente, um sistema de bytecode pode reorganizar mais facilmente o código executado para melhor utilização do cache.

Como um JIT deve renderizar e executar uma imagem binária nativa em tempo de execução, os verdadeiros JITs de código de máquina precisam de plataformas que permitam que os dados sejam executados em tempo de execução, tornando impossível o uso de tais JITs em uma máquina baseada na arquitetura Harvard ; o mesmo pode ser dito para certos sistemas operacionais e máquinas virtuais também. No entanto, um tipo especial de "JIT" pode potencialmente não ter como alvo a arquitetura de CPU da máquina física, mas sim um bytecode VM otimizado onde as limitações no código de máquina bruto prevalecem, especialmente onde o VM desse bytecode eventualmente alavanca um JIT para código nativo.

atuação

O JIT causa um pequeno atraso perceptível na execução inicial de um aplicativo, devido ao tempo necessário para carregar e compilar o bytecode. Às vezes, esse atraso é chamado de "atraso no tempo de inicialização" ou "tempo de aquecimento". Em geral, quanto mais otimização o JIT executa, melhor é o código que ele irá gerar, mas o atraso inicial também aumentará. Um compilador JIT, portanto, precisa fazer uma compensação entre o tempo de compilação e a qualidade do código que espera gerar. O tempo de inicialização pode incluir aumento de operações vinculadas a IO, além da compilação JIT: por exemplo, o arquivo de dados de classe rt.jar para a Java Virtual Machine (JVM) é de 40 MB e a JVM deve buscar muitos dados neste arquivo contextualmente grande .

Uma otimização possível, usada pelo HotSpot Java Virtual Machine da Sun , é combinar interpretação e compilação JIT. O código do aplicativo é inicialmente interpretado, mas a JVM monitora quais sequências de bytecode são freqüentemente executadas e as traduz em código de máquina para execução direta no hardware. Para bytecode que é executado apenas algumas vezes, isso economiza o tempo de compilação e reduz a latência inicial; para bytecode executado com frequência, a compilação JIT é usada para ser executada em alta velocidade, após uma fase inicial de interpretação lenta. Além disso, como um programa passa a maior parte do tempo executando uma minoria de seu código, o tempo de compilação reduzido é significativo. Finalmente, durante a interpretação inicial do código, as estatísticas de execução podem ser coletadas antes da compilação, o que ajuda a realizar uma otimização melhor.

A compensação correta pode variar devido às circunstâncias. Por exemplo, a Java Virtual Machine da Sun tem dois modos principais - cliente e servidor. No modo cliente, a compilação e otimização mínimas são realizadas para reduzir o tempo de inicialização. No modo de servidor, uma ampla compilação e otimização são realizadas para maximizar o desempenho quando o aplicativo estiver em execução, sacrificando o tempo de inicialização. Outros compiladores Java just-in-time usaram uma medida de tempo de execução do número de vezes que um método foi executado combinada com o tamanho do bytecode de um método como uma heurística para decidir quando compilar. Outro ainda usa o número de vezes executado combinado com a detecção de loops. Em geral, é muito mais difícil prever com precisão quais métodos otimizar em aplicativos de execução curta do que em aplicativos de execução longa.

O Native Image Generator (Ngen) da Microsoft é outra abordagem para reduzir o atraso inicial. Ngen pré-compila (ou "pré-JITs") bytecode em uma imagem Common Intermediate Language em código nativo de máquina. Como resultado, nenhuma compilação de tempo de execução é necessária. .NET Framework 2.0 fornecido com o Visual Studio 2005 executa o Ngen em todas as DLLs da biblioteca da Microsoft logo após a instalação. A pré-injeção fornece uma maneira de melhorar o tempo de inicialização. No entanto, a qualidade do código que ele gera pode não ser tão boa quanto aquele que é JITed, pelas mesmas razões pelas quais o código compilado estaticamente, sem otimização guiada por perfil , não pode ser tão bom quanto o código compilado JIT no caso extremo: a falta de dados de perfil para conduzir, por exemplo, cache em linha.

Também existem implementações Java que combinam um compilador AOT (à frente do tempo) com um compilador JIT ( Excelsior JET ) ou interpretador ( GNU Compiler for Java ).

Segurança

A compilação JIT basicamente usa dados executáveis ​​e, portanto, apresenta desafios de segurança e possíveis explorações.

A implementação da compilação JIT consiste em compilar o código-fonte ou código de bytes para o código de máquina e executá-lo. Isso geralmente é feito diretamente na memória: o compilador JIT produz o código de máquina diretamente na memória e o executa imediatamente, em vez de enviá-lo para o disco e, em seguida, invocar o código como um programa separado, como na compilação antes do tempo usual. Em arquiteturas modernas, isso se depara com um problema devido à proteção do espaço executável : a memória arbitrária não pode ser executada, caso contrário, há uma brecha de segurança potencial. Portanto, a memória deve ser marcada como executável; por razões de segurança, isso deve ser feito após o código ter sido escrito na memória e marcado como somente leitura, pois a memória gravável / executável é uma falha de segurança (consulte W ^ X ). Por exemplo, o compilador JIT do Firefox para Javascript introduziu essa proteção em uma versão de lançamento com o Firefox 46.

JIT spraying é uma classe de exploits de segurança de computador que usam a compilação JIT para heap spraying : a memória resultante é então executável, o que permite um exploit se a execução puder ser movida para o heap.

Usos

A compilação JIT pode ser aplicada a alguns programas ou pode ser usada para certas capacidades, particularmente capacidades dinâmicas, como expressões regulares . Por exemplo, um editor de texto pode compilar uma expressão regular fornecida em tempo de execução para o código de máquina para permitir uma correspondência mais rápida: isso não pode ser feito com antecedência, pois o padrão é fornecido apenas em tempo de execução. Vários ambientes de tempo de execução modernos contam com a compilação JIT para execução de código em alta velocidade, incluindo a maioria das implementações de Java , junto com o .NET Framework da Microsoft . Da mesma forma, muitas bibliotecas de expressão regular apresentam compilação JIT de expressões regulares, seja para bytecode ou para código de máquina. A compilação JIT também é usada em alguns emuladores, a fim de traduzir o código de máquina de uma arquitetura de CPU para outra.

Uma implementação comum de compilação JIT é primeiro ter compilação AOT para bytecode ( código de máquina virtual ), conhecido como compilação de bytecode , e então ter compilação JIT para código de máquina (compilação dinâmica), em vez de interpretação do bytecode. Isso melhora o desempenho do tempo de execução em comparação com a interpretação, ao custo do atraso devido à compilação. Os compiladores JIT traduzem continuamente, como ocorre com os intérpretes, mas o armazenamento em cache do código compilado minimiza o atraso na execução futura do mesmo código durante uma determinada execução. Uma vez que apenas parte do programa é compilado, o atraso é significativamente menor do que se todo o programa fosse compilado antes da execução.

Veja também

Notas

Referências

Leitura adicional

links externos