Cómo el compilador JIT optimiza código

Cuando se elige un método para la compilación, la JVM introduce los códigos de bytes en el compilador JIT (Just-In-Time). JIT necesita conocer la semántica y la sintaxis de códigos de bytes para poder compilar el método correctamente.

Para que el compilador JIT pueda analizar el método, primero se reformulan los códigos de bytes en una representación interna denominada árboles, que se parece mucho más al código de máquina que los códigos de bytes. A continuación, se realizan análisis y optimizaciones en los árboles del método. Al final, los árboles se convierten en código nativo. El resto de este apartado ofrece una visión general breve de las fases de la compilación JIT. Para obtener más información, consulte Diagnóstico de un problema JIT o AOT.

El compilador JIT puede utilizar más de una hebra para realizar tareas de compilación de JIT. El uso de varias hebras puede ayudar potencialmente a que las aplicaciones Java se inicien más rápido. En la práctica, la compilación JIT de varias muestra mejoras del rendimiento solo donde existen hay núcleos de procesamiento del sistema sin utilizar.

El número predeterminado de hebras de compilación lo identifica la JVM y depende de la configuración del sistema. Si el número de hebras resultante no es óptimo, puede alterar temporalmente la decisión de la JVM mediante la opción -XcompilationThreads. Para obtener información sobre cómo utilizar esta opción, consulte Opciones -X.
Nota: Si el sistema no tiene núcleos de proceso no utilizados, es poco probable que el aumento del número de hebras de compilación produzca una mejora del rendimiento.

La compilación consta de las fases siguientes. Todas las fases, excepto la generación de código nativo constan de código entre plataformas.

Fase 1 - Incorporación

La incorporación es el proceso mediante el cual los árboles se fusionan o se "incorporan" en los árboles de sus interlocutores. Este proceso acelera con frecuencia las llamadas de método ejecutadas. Se utilizan dos algoritmos de incorporación con diferentes niveles de agresividad, en función del nivel de optimización actual. Las optimizaciones realizadas en esta fase son las siguientes:
  • Incorporación trivial
  • Incorporación de gráficos de llamadas
  • Eliminación de recurrencia de cola
  • Optimizaciones de protección de llamadas virtuales

Fase 2 - Optimizaciones locales

Las optimizaciones locales analizan y mejoran a la vez una pequeña sección del código. Muchas optimizaciones locales implementan técnicas probadas y verificadas empleadas en sistemas estáticos clásicos. Las optimizaciones incluyen:
  • Análisis y optimizaciones de flujos de datos locales
  • Optimización del uso de registro
  • Simplificaciones de lenguajes Java
Estas técnicas se aplican una y otra vez, en especial después de las optimizaciones globales, que pueden haber hecho hincapié en más oportunidades de mejora.

Fase 3 - Optimizaciones del flujo de control

Las optimizaciones de flujo de control analizan el flujo de control que hay dentro de un método (o secciones específicas de él) y reordenan las vías de acceso de código para mejorar su eficacia. Las optimizaciones son:
  • Code reordering, splitting y removal
  • Loop reduction e inversion
  • Loop striding y loop-invariant code motion
  • Loop unrolling y peeling
  • Loop versioning y specialization
  • Optimización dirigida a excepciones
  • Análisis de conmutador

Fase 4 - Optimizaciones globales

Las optimizaciones globales funcionan en todo el método una vez. Son más "caras", requieren mayores cantidades de tiempo de compilación, pero pueden ofrece un mayor rendimiento. Las optimizaciones son:
  • Análisis y optimizaciones de flujos de datos globales
  • Eliminación de redundancia parcial
  • Análisis de secuencias de escape
  • Contexto de gráficos y optimizaciones de asignación de memoria
  • Optimizaciones de sincronización

Fase 5 - generación de código nativo

Los procesos de generación de código nativo varían, en función de la arquitectura de la plataforma. Por lo general, durante esta fase de la compilación, los árboles de un método se convierten en instrucciones del código de máquina; algunas optimizaciones pequeñas se realizan de acuerdo con las características de la arquitectura. El código compilado se coloca en una parte del espacio de procesos de JVM denominado memoria caché de código; la ubicación del método en la memoria caché de código se registra, para que en futuras llamadas a dicho método se llame al código compilado. En cualquier momento determinado, el proceso de la JVM consta de los archivos ejecutables de JVM y un conjunto de código compilado por JIT que está enlazado dinámicamente al intérprete de códigos de bytes en la JVM.