Tenga en cuenta que lo siguiente solo compara la diferencia entre la compilación nativa y JIT, y no cubre los detalles de ningún lenguaje o marco en particular. Puede haber razones legítimas para elegir una plataforma en particular más allá de esto.
Cuando afirmamos que el código nativo es más rápido, estamos hablando del caso de uso típico del código compilado de forma nativa versus el código compilado JIT, donde el uso típico de una aplicación compilada JIT debe ser ejecutado por el usuario, con resultados inmediatos (por ejemplo, no esperando en el compilador primero). En ese caso, no creo que nadie pueda afirmar con franqueza, que el código compilado JIT puede igualar o superar el código nativo.
Supongamos que tenemos un programa escrito en algún lenguaje X, y podemos compilarlo con un compilador nativo, y nuevamente con un compilador JIT. Cada flujo de trabajo tiene las mismas etapas involucradas, que se pueden generalizar como (Código -> Representación intermedia -> Código de máquina -> Ejecución). La gran diferencia entre dos es qué etapas son vistas por el usuario y cuáles son vistas por el programador. Con la compilación nativa, el programador ve todo menos la etapa de ejecución, pero con la solución JIT, el usuario ve la compilación en código máquina, además de la ejecución.
La afirmación de que A es más rápido que B se refiere al tiempo que tarda el programa en ejecutarse, como lo ve el usuario . Si suponemos que ambos fragmentos de código funcionan de manera idéntica en la etapa de ejecución, debemos suponer que el flujo de trabajo JIT es más lento para el usuario, ya que también debe ver el tiempo T de la compilación al código de máquina, donde T> 0. Entonces , para cualquier posibilidad de que el flujo de trabajo JIT funcione igual que el flujo de trabajo nativo, para el usuario, debemos disminuir el tiempo de Ejecución del código, de modo que la Ejecución + Compilación al código de máquina, sea inferior a solo la etapa de Ejecución del flujo de trabajo nativo. Esto significa que debemos optimizar el código mejor en la compilación JIT que en la compilación nativa.
Sin embargo, esto es bastante inviable, ya que para realizar las optimizaciones necesarias para acelerar la ejecución, debemos pasar más tiempo en la etapa de compilación en código de máquina y, por lo tanto, cada vez que ahorramos como resultado del código optimizado se pierde realmente, ya que lo agregamos a la compilación. En otras palabras, la "lentitud" de una solución basada en JIT no se debe simplemente al tiempo adicional para la compilación JIT, sino que el código producido por esa compilación funciona más lentamente que una solución nativa.
Usaré un ejemplo: Asignación de registro. Dado que el acceso a la memoria es miles de veces más lento que el acceso al registro, idealmente queremos usar registros siempre que sea posible y tener la menor cantidad de accesos a la memoria que podamos, pero tenemos un número limitado de registros y debemos verter el estado en la memoria cuando lo necesitemos. un registro Si utilizamos un algoritmo de asignación de registros que requiere 200 ms para calcular, y como resultado ahorramos 2 ms de tiempo de ejecución, no estamos haciendo el mejor uso del tiempo para un compilador JIT. Las soluciones como el algoritmo de Chaitin, que puede producir código altamente optimizado, no son adecuadas.
La función del compilador JIT es lograr el mejor equilibrio entre el tiempo de compilación y la calidad del código producido, sin embargo, con un gran sesgo en el tiempo de compilación rápido, ya que no desea dejar al usuario esperando. El rendimiento del código que se ejecuta es más lento en el caso de JIT, ya que el compilador nativo no está limitado (mucho) por el tiempo en la optimización del código, por lo que es libre de usar los mejores algoritmos. La posibilidad de que la compilación general + la ejecución de un compilador JIT solo supere el tiempo de ejecución para el código compilado de forma nativa es efectivamente 0.
Pero nuestras máquinas virtuales no se limitan simplemente a la compilación JIT. Emplean técnicas de compilación anticipadas, almacenamiento en caché, intercambio en caliente y optimizaciones adaptativas. Así que modifiquemos nuestra afirmación de que el rendimiento es lo que ve el usuario, y limítelo al tiempo necesario para la ejecución del programa (supongamos que hemos compilado AOT). Efectivamente, podemos hacer que el código de ejecución sea equivalente al compilador nativo (¿o quizás mejor?). Un gran reclamo para las máquinas virtuales es que pueden producir código de mejor calidad que un compilador nativo, porque tiene acceso a más información, la del proceso en ejecución, como la frecuencia con la que se puede ejecutar una determinada función. Luego, la VM puede aplicar optimizaciones adaptativas al código más esencial a través del intercambio en caliente.
Sin embargo, hay un problema con este argumento: se supone que la optimización guiada por perfil y similares es algo exclusivo de las máquinas virtuales, lo que no es cierto. También podemos aplicarlo a la compilación nativa: compilando nuestra aplicación con el perfil habilitado, registrando la información y luego recompilando la aplicación con ese perfil. Probablemente también valga la pena señalar que el intercambio en caliente de código no es algo que solo un compilador JIT pueda hacer, podemos hacerlo para el código nativo, aunque las soluciones basadas en JIT para hacerlo están más disponibles y son mucho más fáciles para el desarrollador. Entonces, la gran pregunta es: ¿Puede una VM ofrecernos información que la compilación nativa no puede, lo que puede aumentar el rendimiento de nuestro código?
No puedo verlo yo mismo. También podemos aplicar la mayoría de las técnicas de una máquina virtual típica al código nativo, aunque el proceso es más complicado. Del mismo modo, podemos aplicar cualquier optimización de un compilador nativo a una VM que utiliza compilación AOT u optimizaciones adaptativas. La realidad es que la diferencia entre el código ejecutado de forma nativa y el que se ejecuta en una máquina virtual no es tan grande como se nos ha hecho creer. En última instancia, conducen al mismo resultado, pero adoptan un enfoque diferente para llegar allí. La VM utiliza un enfoque iterativo para producir código optimizado, donde el compilador nativo lo espera desde el principio (y se puede mejorar con un enfoque iterativo).
Un programador de C ++ podría argumentar que necesita las optimizaciones desde el principio, y no debería estar esperando a que una VM descubra cómo hacerlo, si es que lo hace. Sin embargo, este es probablemente un punto válido con nuestra tecnología actual, ya que el nivel actual de optimizaciones en nuestras máquinas virtuales es inferior a lo que pueden ofrecer los compiladores nativos, pero eso no siempre puede ser el caso si las soluciones AOT en nuestras máquinas virtuales mejoran, etc.