Cómo el compilador JIT de Java mejora la velocidad de ejecución

Históricamente, los lenguajes siempre se habían dividido entre interpretados y compilados, en lo que se refiere a su ejecución. A día de hoy, con los compiladores JIT se crea una nueva categoría: los lenguajes que son una combinación de ambos. Los compiladores JIT son omnipresentes, por ejemplo los navegadores modernos disponen de uno para el lenguaje Javascript. También las máquinas virtuales de Java han evolucionado para incluir esta técnica, y es el caso concreto en el que se va a profundizar en este artículo.

Las implementaciones modernas de Java emplean un proceso de compilación dividido en dos pasos en el cual se produce una compilación en tiempo de ejecución, es decir, las máquinas virtuales de Java actuales disponen de un compilador JIT, just-in-time por sus siglas en inglés. En primer lugar, el compilador de Java compila en bytecode el código fuente, este bytecode es independiente de la plataforma de hardware donde se ejecuta. La máquina virtual de Java (JVM) es la encargada de ejecutar (interpretar mediante un intérprete) este bytecode. La máquina virtual sí es dependiente de la plataforma: Cada plataforma requiere de su propia máquina virtual para poder ejecutar el bytecode.

El compilador JIT de la máquina virtual es el encargado de detectar partes de este bytecode que se están ejecutando con mucha frecuencia y compilarlas al código máquina de la CPU de la plataforma para aumentar la velocidad de ejecución. El componente del compilador JIT encargado de detectar estos «puntos calientes» («hotspots» o «hot code» en inglés) del código es el profiler. Además, lo realiza en tiempo de ejecución, es decir, a medida que el programa se está ejecutando, detecta cuáles nuevas partes del código se han calentado y cuáles se han enfriado. De esta manera se supera la principal ineficiencia de los intérpretes: cuando código que se está ejecutando de forma reiterada (por ejemplo debido a un bucle while / for) debe ser interpretado una y otra vez. Además, esta característica es realmente potente, pues consigue que determinados tipos de programas se ejecuten más rápido que su equivalente en un lenguaje compilado orientado a objetos.

Para ver un caso concreto de cómo se optimiza en tiempo de ejecución un programa realizado en Java, pongamos a trabajar al ordenador: calculará 100 veces el número situado en la duodécima posición de la serie de Fibonacci, una forma como otra cualquiera de poner a calcular un ordenador. Este número es el 144, pero lo que realmente nos interesa es el tiempo transcurrido en cada una de las 100 ejecuciones.

fibonacci en JavaEn primer lugar, se ejecuta el programa con el compilador JIT desactivado:

$ java -Djava.compiler=NONE fibonacci.java
32715
57063
52927
62734
55639
51689
52716
33706
43873
52316
33760
33804
33869
34189
34024
33816
34075
34227
34046
34165
34035
33947
34186
34299
34045
34056
34151
34166
34357
34246
34154
33979
33983
33977
34736
34260
33801
34146
33923
34409
34164
33880
34148
34061
34231
33965
34385
34042
33939
33858
33873
34103
33957
34017
34246
34065
34034
33925
33891
34182
33876
34206
33908
34397
34147
33879
34062
34351
49945
34102
34965
33966
42351
34126
33659
33890
33997
34335
33968
34193
34015
34399
33911
34034
33690
33699
33719
33792
33970
33735
33791
33884
33847
34066
33871
33847
33679
34216
33773
33882

Cada uno de estos números representa la cantidad de nanosegundos consumidos para calcular la duodécima posición de la serie. A continuación, se ejecuta el programa normalmente, es decir, con el compilador JIT activo:

$ java fibonacci.java
43069
38093
7849
2312
1527
1957
1528
1486
1491
1981
1491
1486
1896
1476
1882
1465
1471
1491
1475
1472
6303
1523
1482
1494
1460
1967
1501
1892
1491
1918
1473
1926
1484
1491
1486
1469
1479
1501
1521
1494
1506
1524
1955
1551
1464
1875
1502
1928
1491
1475
1467
1456
1490
1468
1461
1465
1475
1466
1460
1478
1467
1473
1493
1462
1927
1508
1472
1499
1491
1505
1485
2131
1468
1474
1461
1482
1473
1467
1469
1470
1491
1490
1933
1473
1949
1498
1934
1471
1492
1486
1464
1497
1462
1460
1484
1482
1491
1477
1472
1457

Lo interesante sucede en las primeras 5 iteraciones. De la iteración inicial que consume 43069 nanosegundos se pasa a los 38093ns de la segunda. La mejoría destacable sucede en la tercera, cuarta y quinta iteración, donde el tiempo se rebaja a 7849, 2312 y 1527ns, respectivamente. Probablemente esto sea debido a la detección por parte del compilador JIT de la función fibonacci() como código caliente y su compilación. A partir de ahí, excepto en una ocasión, todas las iteraciones se mantienen por debajo de los 2000ns.

En definitiva, la incorporación del compilador de JIT a la máquina virtual de Java hace que el lenguaje ya no pueda clasificarse como estrictamente interpretado o compilado. Asimismo, se ha podido comprobar mediante un sencillo ejemplo como es capaz de mejorar sensiblemente los tiempos de ejecución. De hecho, algunos programas consiguen tiempos de ejecución en Java superiores a su equivalente en C++, un lenguaje compilado, incluso si la compilación se realiza con las opciones de optimización de nivel 2 del compilador gcc.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.