Como o Compilador JIT Otimiza Código

Quando um método é escolhido para compilação, a JVM alimenta seus bytecodes ao compilador Just-In-Time (JIT). JIT precisa entender a semântica e a sintaxe dos bytecodes antes de poder compilar o método corretamente.

Para ajudar o compilador JIT a analisar o método, seus bytecodes são primeiramente reformulados em uma representação interna chamada árvores, que se assemelha a código de máquina mais do que bytecodes. Análise e otimizações são executadas então nas árvores do método. No final, as árvores são convertidas em código nativo. O restante desta seção fornece uma visão geral breve das fases de compilação JIT. Para obter mais informações, consulte Diagnosticando um problema de JIT ou AOT

O compilador JIT pode usar mais de um encadeamento de compilação para executar tarefas de compilação JIT. Usar vários encadeamentos pode potencialmente ajudar os aplicativos Java a iniciarem mais rapidamente. Na prática, diversos encadeamentos de compilação JIT mostrarão melhorias de desempenho somente onde houver núcleos de processamento não usados no sistema.

O número padrão de encadeamentos de compilação é identificado pela JVM e depende da configuração do sistema. Se o número resultante de encadeamentos não for ideal, é possível substituir a decisão da JVM usando a opção -XcompilationThreads. Para obter informações sobre como usar essa opção, consulte Opções -X..
Nota: Se seu sistema não tiver núcleos de processamento não utilizados, é improvável que o aumento do número de encadeamentos de compilação produza uma melhoria de desempenho.

A compilação consiste nas seguintes fases. Todas as fases, exceto a geração de código nativo são código para diversas plataformas.

Fase 1 - Inlining

Inlining é o processo pelo qual as árvores de métodos menores são mescladas, ou colocadas em "inline", nas árvores de seus responsáveis pelas chamadas. Isso acelera chamadas de métodos executadas com frequência. Dois algoritmos de inlining com diferentes níveis de agressividade são usados, dependendo do nível de otimização atual. As otimizações executadas nesta fase incluem:
  • Inlining trivial
  • Inlining de gráfico de chamada
  • Eliminação de recursão de cauda
  • Otimizações de guarda de chamada virtual

Fase 2 - Otimizações Locais

As otimizações locais analisam e melhoram uma pequena seção do código por vez. Muitas otimizações locais implementam técnicas experimentadas e testadas usadas em compiladores estáticos clássicos. As otimizações incluem:
  • Análise e otimizações de fluxo de dados locais
  • Otimização de uso do registro
  • Simplificações de Idiomas Java
Essas técnicas são aplicadas de forma repetitiva, principalmente após otimizações globais, que podem ter apontado mais oportunidades para melhoria.

Fase 3 - Otimizações do Fluxo de Controle

As otimizações do fluxo de controle analisam o fluxo de controle dentro de um método (ou seções específicas do mesmo) e reorganizam os caminhos de código para melhorar a eficiência dos mesmos. As otimizações são:
  • Reordenação, divisão e remoção do código
  • Redução e inversão do loop
  • Movimentação de percurso do loop e de código invariante do loop
  • Desenrolar e descascar do loop
  • Versão e especialização do loop
  • Otimização direcionada por exceção
  • Análise do comutador

Fase 4 - Otimizações Globais

As otimizações globais funcionam em todo o método de uma vez. São mais "caras", exigindo maiores quantidades de tempo de compilação, mas podem fornecer um grande aumento de desempenho. As otimizações são:
  • Análise e otimizações do fluxo de dados global
  • Eliminação de redundância parcial
  • Análise de escape
  • Otimizações de GC e alocação de memória
  • Otimizações de sincronização

Fase 5 - Geração de Código Nativo

Os processos de geração de código nativo variam, dependendo da arquitetura da plataforma. Em geral, durante essa fase da compilação, as árvores de um método são convertidas em instruções de código de máquina; algumas pequenas otimizações são executadas de acordo com as características da arquitetura. O código compilado é colocado em uma parte do espaço do processo da JVM chamado de código de cache; o local do método no cache de código é registrado, de forma que chamadas futuras a ele chamarão o código compilado. A qualquer tempo, o processo da JVM consiste nos arquivos executáveis da JVM e um conjunto de código compilado JIT que é vinculado dinamicamente para o interpretador de bytecode na JVM.