compiler-construction jvm clr language-design

compiler construction - ¿Razón de las opciones de diseño que hacen que los idiomas de JVM/CLR tengan un inicio prolongado?



compiler-construction language-design (4)

Aquí hay un resumen de algunas de las otras respuestas que han dicho.

Los posibles compromisos de diseño son:

  • latencia vs. rendimiento
  • precompilación de algunos módulos de biblioteca vs. biblioteca estándar solo en bytecode
  • importaciones de módulo de carga lenta
  • seguridad vs. eficiencia
  • Eficiencia de la organización en disco de módulos.

Parece poco probable que todos estos sean igual de importantes, pero sin experimentación, es difícil saber cuál de ellos contribuye más a la latencia de inicio de las aplicaciones que se ejecutan en tiempos de ejecución administrados.

Con más detalle:

latencia vs. rendimiento

Si está dispuesto a que la inicialización tarde mucho tiempo, puede ir más rápido más tarde. Algunas cosas que puede optar por hacer durante la inicialización para mejorar la velocidad posterior son:

  • compilación de tiempo de carga (o compilación parcial)
  • tiempo de carga de enlace
  • cargar con entusiasmo la biblioteca estándar
  • Posiblemente, haga un trabajo extra mientras inicializa el GC

La JVM carga el enlace de tiempo ( deniss ). Muchas implementaciones de JVM realizan una compilación parcial durante el tiempo de carga o la inicialización ( deniss , mb21 ). CPython proporciona una gran biblioteca estándar, pero solo carga previamente los módulos esenciales en el momento del inicio ( delnan ); answer implica que Java carga perezosamente su biblioteca estándar, pero Clojure carga su biblioteca con entusiasmo. Mike Nakis sugiere que la JVM puede pasar mucho tiempo inicializando el GC.

precompilación de algunos módulos de biblioteca en lugar de la biblioteca estándar solo en bytecode, solo interpretados y separados del ejecutable principal

Hay varias formas de compilar previamente un conjunto de módulos de biblioteca:

  • su bytecode podría ser compilado en tiempo de carga
  • el tiempo de ejecución podría cargar un conjunto de bibliotecas una vez, volcar una imagen de su memoria, y luego la próxima vez simplemente cargar en esta imagen
  • Se podrían compilar en el ejecutable principal de la máquina virtual.
  • podrían escribirse en un idioma diferente y compilarse en bibliotecas binarias que la VM carga dinámicamente en tiempo de ejecución

Aparte de la mayor complejidad de la implementación, no puedo pensar en ningún compromiso de diseño con los tres primeros. El último implica una compensación, porque evitar el código nativo y escribir la biblioteca estándar en el idioma tiene otras ventajas: (a) portabilidad (b) los desarrolladores no tienen que saber cómo escribir código nativo para contribuir al estándar bibliotecas (c) uso de la biblioteca estándar como código de muestra (d) las bibliotecas estándar no requieren un FFI, ni requieren otro tratamiento especial (como lo harían si fueran nativos pero otras bibliotecas no podrían serlo).

CPython se envía con algunos módulos de biblioteca estándar compilados en su ejecutable principal ( delnan ). Algunas implementaciones de JVM proporcionan " 6 ", que parece ser un sistema para almacenar en caché una imagen de aquellas partes de la biblioteca estándar de Java que siempre se cargan en el inicio. Sin embargo, el "intercambio de datos de clase" de la JVM aparentemente no se puede usar fácilmente para precompilar bibliotecas de usuarios como las bibliotecas estándar de Clojure, lo que significa que otros idiomas en la JVM además de Java están fuera de suerte. Algunos otros idiomas, como SBCL, proporcionan una facilidad para volcar una imagen del estado actual del intérprete ( 20 ).

importaciones de módulo de carga lenta

answer parece estar diciendo que la distinción entre el tiempo de inicio de Java y el tiempo de inicio de Clojure se debe a que Java carga los módulos de la biblioteca, mientras que Clojure lo carga con entusiasmo. Excepto por el aumento de la complejidad (lo cual es importante), no puedo pensar en una razón de diseño por la que cualquier idioma no deba soportar (al menos opcional) la carga perezosa de módulos.

seguridad vs. eficiencia

Un lenguaje puede desear proporcionar ''sandboxing'', en el que se pueda ejecutar código no confiable con capacidades limitadas. Esto requiere la verificación del código antes de ejecutarlo o la inserción dinámica de cheques antes de acciones potencialmente restringidas ( Mike Nakis ). En el último caso, si una de estas acciones es cargar un módulo, esta comprobación dinámica puede realizarse muchas veces al iniciarse, lo que se suma al tiempo de inicio.

De manera similar, un lenguaje puede desear garantizar que incluso el código de bytes escrito por un adversario no puede "fallar" de una manera incontrolada que devuelve el control al sistema operativo, pero en el peor de los casos solo puede causar una excepción, transferir el control a través del controlador de excepciones en el idioma. Esto requiere un lenguaje de bytecode que no puede expresar de manera sintáctica situaciones causantes de fallos, como accesos ilegales a la memoria, o requiere hacer verificaciones dinámicas en el tiempo de ejecución antes de cada acción potencialmente peligrosa, o requiere la verificación del bytecode en el momento de la carga para demostrar que no puede causar un fallo ( deniss ). La última de estas opciones aumenta el tiempo de carga de cada módulo, lo que aumenta el tiempo de inicio.

La JVM proporciona una zona de pruebas a través de comprobaciones dinámicas y verificación de bytecode a través de una verificación de tiempo de carga; Ambas opciones frenan la carga de los módulos de la biblioteca en el inicio hasta cierto punto, aunque quizás sean muy pequeños.

Eficiencia de la organización en disco de módulos.

Las bibliotecas de JVM se distribuyen en muchos archivos porque son de una clase por archivo. Esto aumenta el tiempo de carga en comparación con poner muchas clases en un archivo ( 11 , Mike Nakis ). Las bibliotecas JVM también están codificadas en archivos .jar, que utilizan el formato de archivo ZIP. Otro formato de archivo podría haber sido más eficiente para cargar, especialmente en aquellos casos en los que solo se necesitan unas pocas clases de un gran .jar con muchas clases ( 15 , Mike Nakis ).

Además, el bytecode JVM no admite constantes compuestas en el bytecode , lo que significa que las constantes compuestas deben construirse en tiempo de ejecución a partir de una secuencia de instrucciones interpretadas que se ejecutan en la inicialización. (Sin embargo, creo que CPython es de la misma manera ). Permitir constantes compuestas en el código de bytes requeriría que la especificación de la máquina virtual tuviera que incluir un formato para la serialización de la estructura de datos compuestos, un aumento significativo en la complejidad de la especificación.

Estoy pensando en diseñar un lenguaje de programación y me gustaría iniciarlo con aproximadamente la misma velocidad que CPython o Perl. Con el fin de tomar las decisiones de diseño correctas en mi idioma para cumplir con este requisito, estoy analizando los idiomas dinámicos existentes para ver cómo sus opciones de diseño afectan su tiempo de inicio. Muchas implementaciones de lenguaje basadas en JVM o CLR tienen un tiempo de inicio mucho más largo que CPython o Perl. Esto sugiere que se hizo una elección de diseño en el diseño de la JVM y / o CLR que causa esto. ¿Cuál fue esa elección y por qué se hizo de esa manera?

Esta es una pregunta de tres partes:

  1. ¿Es el inicio lento de las implementaciones dinámicas de lenguaje JVM / CLR un problema de diseño fundamental , o solo un problema menor que podría corregirse al mejorar las implementaciones de lenguaje?
  2. Si se trata de un problema de diseño, ¿ qué opciones de diseño de la JVM y qué opciones de diseño de estos idiomas hacen que estos idiomas tengan una latencia de inicio más larga que CPython y Perl?
  3. ¿Qué se gana a cambio de ser lento para comenzar? Es decir, ¿qué beneficios tienen los lenguajes dinámicos JVM / CLR que carecen de CPython y Perl, debido a las opciones de diseño descritas en (2)?

Tenga en cuenta que otras preguntas de SO ya tratan con " ¿Por qué la JVM tarda en iniciarse? " Y por qué los distintos lenguajes de JVM son lentos para arrancar. Esta pregunta es distinta de esta porque esta pregunta es sobre el compromiso de diseño; ¿Qué se gana a cambio de ese largo tiempo de inicio?

Otras preguntas de SO preguntan cómo el usuario puede acelerar varios idiomas de JVM (y la respuesta a menudo es tener algún tipo de demonio que cargue un JVM), pero eso no es lo que estoy preguntando aquí; Le pregunto cómo diseña un idioma (y / o máquina virtual) que permita un inicio rápido (sin precarga), y qué pierde a cambio de esto.

Investigación de fondo

Velocidad de varias implementaciones de lenguaje.

Realicé pruebas comparativas de CPython y Perl en las pruebas informales de Hello World en mi máquina GNU / Linux, y descubrí que comienzan en menos de 0.05 segundos. En el resto de esta publicación, diré que "rápido" significa "tiempo de inicio que no es significativamente más largo que el de CPython o Perl", y que "lento" significa lo contrario.

Es fácil encontrar opiniones de que la JVM y / o Java tardan en iniciarse ( 3 , 4 , 5 , 6 ), así como números concretos del orden de 1 segundo o más ( 7 , 27 ) y puntos de referencia ( 8 ). Sin embargo, dos pruebas comparativas Hello World JVM se iniciaron en solo 0.04 segundos (en GNU / Linux) ( 9 , 10 ).

Clojure tuvo un tiempo de inicio de alrededor de 0.6-1 segundos ( 10 , 2 ); esto es aproximadamente 20x más lento que mi objetivo de 0.05 segundos. ClojureCLR es incluso más lento ( 10 ). Los puntos de referencia y la discusión del tiempo de inicio de Clojure se pueden encontrar en las publicaciones del blog 2 , 10 .

Un punto de referencia en el momento de la puesta en marcha dijo que Clojure y JRuby eran "significativamente más lentos que todo lo demás" ( 25 ); estos también fueron los únicos dos lenguajes dinámicos basados ​​en JVM probados. Otro (muy antiguo) punto de referencia muestra que Jython también es muy lento para comenzar ( 26 ). Nos estamos enfocando en lenguajes dinámicos en esta pregunta, sin embargo, puede ser relevante que Scala tampoco sea increíblemente rápido ( 10 ). Hay un esquema JVM llamado Kawa ( 23 ). Se informó que el tiempo de inicio de Kawa era de aproximadamente 0,4 ( 24 ), que es más rápido que en Clojure pero sigue siendo un orden de magnitud por encima de mi objetivo.

¿Qué están haciendo las implementaciones durante el inicio?

Ambos ( 10 , 2 ) concluyen que Clojure está gastando sus clases de tiempo de inicio e inicializando el espacio de nombres clojure.core. Una answer a la pregunta SO "Rendimiento de inicio de la aplicación Clojure" parece estar diciendo que la distinción entre el tiempo de inicio de Java y el tiempo de inicio de Clojure se debe a que Java carga perezosamente su biblioteca estándar, mientras que Clojure lo carga con entusiasmo. Las respuestas a la pregunta SO " ¿Puede cualquier implementación de Clojure comenzar rápidamente? " Incluye "es solo un problema de implementación que podría corregirse, no una elección de diseño fundamental" ( paraphrased ), y "Una limitación de la JVM es que los objetos deben copiarse en inicialización, "No se pueden incrustar constantes compuestas en el código de byte. Ni siquiera matrices. "" ).

Una publicación del blog afirma que el tiempo de inicio de ClojureCLR se dedica principalmente a JITing, y el pre-JITing redujo drásticamente el tiempo (aunque todavía puede ser lento en comparación con CPython y Perl).

Una explicación proporcionada por qué algunos programas JVM o Java tardan en iniciarse es la E / S de carga en muchos archivos de clase de una biblioteca estándar ( 11 ) . Esta hipótesis está respaldada por puntos de referencia que muestran una mejora drástica en el tiempo de inicio de la JVM para los "inicios en caliente" donde, presumiblemente, los contenidos de los archivos de clase de la biblioteca estándar ya se han cargado en el caché del sistema operativo. Algunos dicen que gran parte del tiempo de inicio se debe a la lectura de E / S en los archivos de clase, pero no debido al gran volumen de datos, sino más bien a la organización subóptima de esos datos en el disco ( 15 , 11 ).

El verificador de bytecode de JVM probablemente no contribuya significativamente al tiempo de inicio , porque el 40% de aceleración del verificador solo se tradujo a un 5% de aceleración del tiempo de inicio del programa grande ( 3 ).

¿Qué opciones de diseño (no) llevan a un inicio lento?

En ( 22 ), Kariniemi llega a la conclusión de que el inicio de Clojure es intrínsecamente lento para iniciar debido a la opción de diseño de incluir características dinámicas. Sin embargo, cuestiono esta conclusión porque CPython y Perl logran un inicio mucho más rápido al mismo tiempo que proporcionan dinamismo.

El uso de bytecode no puede ser la causa, porque CPython también usa bytecode.

Debido a que la E / S de la carga de los archivos de clase parece estar en falta, uno podría sospechar que la opción de diseño subyacente es la provisión de una gran biblioteca estándar. Sin embargo, esto no puede ser la causa, ya que CPython también proporciona una biblioteca estándar grande y no tarda en iniciarse. Además, aunque la lentitud de Java está en disputa, vale la pena señalar que Java debe cargar rt.jar en el inicio, pero Hello World se ejecuta rápidamente en Java de acuerdo con algunos puntos de referencia.


El metanálisis de los estudios de otras personas que intentas hacer no es una buena idea en mi opinión. No tiene conocimiento de las variaciones entre las configuraciones de estas personas, por lo que puede estar comparando manzanas con naranjas, y no tiene garantías sobre la validez de la metodología que sigue cada una de ellas, por lo que las manzanas pueden estar podridas, las naranjas pueden de hecho, sean clementinas, y puede que se lance una pera ocasional: usted no lo sabría.

La respuesta a sus preguntas es que la mayor parte del tiempo de inicio lento se debe a las opciones de diseño fundamentales que realmente queremos tener, y solo una pequeña parte de esto se debe a las opciones de diseño no fundamentales, sino simplemente desafortunadas, por las cuales estamos atrapados. Razones de compatibilidad y que probablemente se pueden evitar con un mejor diseño. Afortunadamente, la penalización de rendimiento de las opciones de diseño fundamentales que realmente queremos tener puede reducirse mediante optimizaciones posteriores al hecho, pero las personas que trabajan en estas cosas tienden a ser bastante inteligentes, por lo que la mayoría de las cosas que podrían mejorarse han mejorado. Probablemente ya ha sido mejorado.

Un ejemplo de una elección de diseño fundamental es el uso de un lenguaje intermedio como bytecode o msil, lo que significa que durante el inicio pagará la penalización de ejecutar un compilador de optimización.

Un ejemplo de una elección de diseño no fundamental pero desafortunada es la elección de una clase por archivo de java, que fue concebida en el día en que java estaba destinada a ejecutarse dentro de páginas web en el lado del cliente: esto reduce significativamente el inicio tiempo de una sola clase, pero introduce una gran penalización de rendimiento adicional cada vez que haces algo que requiere que se carguen más clases. En el momento en que ha cargado suficientes clases para completar su inicio, generalmente ha pagado una multa de rendimiento total mucho mayor que si hubiera esperado para cargar todas estas clases a la vez.

Aquí hay una lista de problemas en los que puedo pensar, que afectan el tiempo de inicio. La mayoría de ellos son bastante importantes, por lo que sería inútil explicar qué se gana con cada uno de ellos.

  1. El uso de un lenguaje intermedio. Básicamente, está pasando toda su aplicación y partes del tiempo de ejecución a las que hace referencia a través de un compilador de optimización cada vez que lo inicia. Esto puede optimizarse almacenando en caché los binarios en la instalación, o en la primera ejecución, y reutilizándolos en ejecuciones posteriores. Supongo que algunos tiempos de ejecución ya lo están haciendo, o de lo contrario no es tanto un problema.

  2. El uso de un recolector de basura. Este es un software bastante complejo, por lo que no me sorprendería si tuviera una inicialización algo costosa. Por ejemplo, el recolector de basura generalmente se ejecuta en un subproceso propio, lo que significa que el inicio de la aplicación tiene la carga de iniciar al menos un subproceso. (Que debe tener su propia pila, etc.) Esto podría optimizarse lanzando el subproceso extra del recolector de basura solo si es necesario, por lo que las aplicaciones que terminan antes de asignar mucha memoria nunca lo necesitarían. Además, muchas aplicaciones GUI en teoría podrían hacer toda su recolección de basura durante el tiempo de inactividad, por lo que realmente no necesitan un hilo adicional para esto, aunque el tiempo de inicio de las aplicaciones GUI no suele ser una preocupación.

  3. El uso de una máquina virtual aislada. Esto significa que, inmediatamente después del lanzamiento, la máquina virtual debe asignar previamente una gran cantidad de memoria para trabajar. La asignación masiva de memoria generalmente incurre en una penalización de rendimiento significativa, pero nos ahorra asignaciones adicionales de trozos pequeños posteriores al inicio, que pueden sumar una penalización de rendimiento general mucho mayor en el largo plazo. Este suele ser un problema de la arquitectura del hardware / sistema operativo, y no me sorprendería si Linux hace un trabajo mucho mejor que Windows. Una forma de optimizarlo es establecer una forma de determinar el final de su tiempo de inicio, tomando nota de cuánta memoria se requiere, y luego, en los lanzamientos subsiguientes, indíquele a la VM que preasigne exactamente esa memoria: nada menos, para no incurrir en la penalización de asignaciones adicionales, y no más, para no perder tiempo asignando memoria que no será necesaria. Por supuesto, debe ser inteligente al respecto, ya que la memoria que su aplicación deberá asignar al inicio a menudo depende de los argumentos de la línea de comandos, que probablemente sean diferentes de una ejecución a otra. Como de costumbre, no hay bala de plata.

  4. Consideraciones de Seguridad. Esta es un área en la que tengo poca experiencia, por lo que realmente no puedo proporcionar mucha información. Sé que cuando mi programa Java se ejecuta, cada byte de mi código de bytes es examinado por algún verificador, y sé que cada vez que intento hacer algo tan inocente como cargar una clase, se invoca a un gerente de seguridad para que opine sobre si Se debe permitir que lo haga. Por mi vida, no sé por qué la JVM se molesta con todo esto, en lugar de dejar que el sistema operativo maneje un proceso que falla o un proceso que carece de privilegios suficientes para hacer su trabajo. Algunas veces me entretengo con la idea de que no es más que un plan de marketing para agregar la palabra de moda "seguridad" como uno de los puntos de venta del idioma, pero, por supuesto, solo dentro de una etiqueta <joking></joking> . Sospecho firmemente que podría eliminar toda esta "seguridad", pero asegúrese de consultar también con otros sobre este tema.

  5. La organización de los archivos de clase. Java usa archivos JAR, que son esencialmente archivos ZIP, por lo que la primera vez que se necesita una clase de un archivo JAR, se debe analizar el archivo completo y, posiblemente, todo ello descomprimido e indexado, incluso si nunca se necesitará otra clase. eso. Estoy seguro de que las JVM optimizan esto tanto como pueden, pero aún así, estaría dispuesto a apostar a que una opción diferente podría dar mejores resultados. El beneficio es, por supuesto, que puede abrir sus archivos JAR con su utilidad ZIP. El CLR utiliza ensamblajes de DLL, cuya estructura no conozco, pero estaría dispuesto a apostar a que se desempeñen mejor (o que potencialmente puedan hacerlo).

  6. La decisión fundamental de proporcionar un entorno de tiempo de ejecución rico y la cultura de programación de reutilizar las características existentes para resolver problemas en lugar de proporcionar soluciones posiblemente óptimas pero ad hoc. En C, tu "¡Hola mundo!" El programa pasaría a la printf() un puntero a una matriz estática de caracteres. Nada puede funcionar mejor que eso. En Java, se creará una instancia del objeto String , se asignará memoria adicional para una matriz de caracteres, los caracteres se copiarán en esa memoria y luego se pasará un puntero al objeto String a System.out.println() función. La clase String es bastante compleja, y una gran parte de su complejidad tiene que ver con el rendimiento posterior al inicio. (Ver cadena interna). Luego, la función println() probablemente convertirá los caracteres Unicode a ansi, lo que significa que se cargarán varias clases de conversión de texto, posiblemente junto con tablas de conversión Unicode, que son de un tamaño no despreciable. . Estas clases de conversión de texto probablemente utilizarán las colecciones estándar, donde solo el HashMap es de alrededor de 50 KB de bytecode. Usted ve a dónde va esto.


El tiempo de inicio está determinado por la cantidad de trabajo necesario para el tiempo de ejecución antes de que realmente pueda comenzar a ejecutar cualquier ''código de usuario''. Permítanme comparar qué sucede exactamente con algunas elecciones.

Binario nativo (C ++ o así)

El sistema operativo maps archivo ejecutable principal a la memoria. Incluso si este archivo es muy grande (unos pocos GB), el mapeo sigue siendo bastante rápido. Y es muy rápido para el tamaño de archivo típico de 10-50MB. Luego se lee un encabezado ejecutable, que proporciona una lista de módulos dinámicos. Esos módulos se buscan por sistema operativo y se asignan de la misma manera. Entonces, posiblemente, se realicen algunas reubicaciones. Después de esto, su código estará listo para ejecutarse (aunque el control en este punto probablemente se otorgue al tiempo de ejecución de su idioma, no al código en sí).

Lenguajes de scripting

Después de que todo lo descrito en la sección anterior pase al ejecutable del intérprete, comienza a leer y ejecutar el script proporcionado. Supongamos que no se realiza ningún análisis / compilación de bytecode (ya tenemos todo en formato .pyc o bytecode similar). Con cada módulo que el intérprete necesita cargar, simplemente asigna suficiente espacio de memoria y copia el contenido del módulo en él. Luego transfiere el control a este fragmento de bytecode. De hecho, se debe hacer algún trabajo en esta etapa, pero generalmente no mucho. Por ejemplo, ese es un bytecode de dis módulo dis de Python que se ejecutará en la import dis . De import dis .

Yendo a JVM

Para JVM, no es tan fácil. Primero, el tiempo de ejecución no puede simplemente ''asignar'' el archivo de .class a la memoria, ni leer su contenido en la memoria y decirle al intérprete: "¡Oye! Aquí está tu código de bytes". Necesita ser verificado y resuelto .

El propósito de la verificación es asegurarse de que el intérprete pueda ejecutarse sin más comprobaciones en tiempo de ejecución (ramas fuera de función, desbordamiento o subdesbordamiento de la pila, verificación de tipos). Incluso si asumimos el tiempo O (número de instrucción) para la verificación, todavía es bastante, ya que cada instrucción individual en el módulo debe ser verificada. Recuerde, para el lenguaje de scripting tenemos una pequeña cantidad de trabajo en la carga, generalmente solo para rellenar el diccionario de ''exportación'' con nuevas funciones y clases.

Resolver es un tipo de optimización (y elección de diseño de idioma). Considere el código java:

System.out.println("Hello, world!");

Para este código, el compilador java coloca en el archivo .class información sobre println : println es un método estático, con firma (ILJAVA/LANG/STRING;)V , de la clase java.lang.System . Cuando se carga una clase que contiene la línea anterior, JVM debe buscar java.lang.System (posiblemente también cargarlo en el proceso), encontrar el método println con esta firma y colocar el puntero a este método en algún lugar para que luego se pueda encontrar cuando Esta línea pasa a ejecutar.

Este procedimiento debe realizarse para cada invocación de método único en cada clase cargada. Lo mismo para cada clase referenciada, campo, interfaz y así sucesivamente. Por lo tanto, cargar un archivo .class grande no se trata de "copiar su contenido en la memoria" y "realizar algún ajuste de entorno".

Con una biblioteca estándar lo suficientemente grande, esas operaciones por sí solas pueden llevar a un largo tiempo de inicio.

La compilación (al menos la optimización de la compilación) es lenta. Recuerde cuánto tiempo puede llevar compilar un proyecto de C ++ de tamaño decente. Así que varios trucos utilizados para hacer esta operación más rápida. En JVM (al menos en algunas implementaciones), la interpretación y la compilación de JIT se pueden realizar en paralelo, de modo que el código se interpreta de forma predeterminada y el JIT si se determina que está "activo" (se ejecuta a menudo). Pero la interpretación también es lenta. Así que no es una bala mágica, solo un intercambio entre "hacer las cosas con calma" o "no hacerlas en absoluto y espero que JIT termine su trabajo pronto". Python sin jit solo soporta "hacerlo lento". Pero algunas partes críticas del rendimiento de la biblioteca estándar (como los diccionarios) están escritas en C (o Java, o C #, no en Python). La biblioteca estándar de Java está escrita en Java. Es por eso que también se debe compilar JIT, o se ejecutará lentamente.

Resumen

  1. Estos tiempos de inicio lentos son un problema de diseño. Ese es el precio de ser ''casi tan rápido como C'' y altamente dinámico al mismo tiempo.

  2. Las opciones de diseño que conducen a esta desaceleración son: la verificación del código de bytes en el tiempo de carga en lugar del tiempo de ejecución, la vinculación del tiempo de carga y la compilación JIT.

  3. Como dije, esto permite que JVM genere código con JIT, que es casi tan rápido (e incluso más rápido en algunas pruebas) que el código en un idioma nativo.

Conclusión

Si desea un tiempo de inicio bajo, diseñe su idioma de tal manera que el tiempo de ejecución no tenga que hacer mucho trabajo para cargar un módulo. En el mejor de los casos, no hay más trabajo que copiar + actualización del entorno.

La sobrecarga de JIT puede ser superada con la caché JIT, o la compilación anticipada de tiempo . Ese no es el caso si su idioma es completamente dinámico (por ejemplo, puede anular la propiedad ''array.length'' en algunos módulos y la biblioteca estándar también debe respetar este cambio).


En primer lugar, la mayoría de los lenguajes pueden, en principio, ser compilados a binarios, o ejecutarse en tiempo de ejecución (o algo intermedio, como JIT). Entonces, lo que realmente está preguntando no es tanto sobre el diseño del lenguaje, sino sobre el diseño del compilador y el tiempo de ejecución (vea Compiled vs. Interpreted Languages ).

Ahora, en términos generales, hay que considerar tres etapas. Básicamente, tendrá que elegir en cuál de esas etapas desea pasar más tiempo y en qué menos, siempre va a ser un intercambio.

  1. Tiempo de compilación
  2. Inicialización
  3. Tiempo de ejecución

Los lenguajes que normalmente se compilan, como C, pasan mucho tiempo en (1) para optimizar el binario que se genera. Un compilador puede hacer una de dos cosas: o intenta hacer que el binario sea muy pequeño, de modo que cuando se inicia el programa, se carga rápidamente desde el disco duro a la RAM, lo que minimiza el tiempo de inicialización (2). Pero más comúnmente, los compiladores tienden a no preocuparse tanto por el tamaño binario y, en cambio, intentan optimizar el rendimiento en tiempo de ejecución (3). La incorporación de funciones es un ejemplo clásico de optimización del rendimiento en tiempo de ejecución a expensas del tamaño binario.

En el otro extremo, el código en lenguajes que generalmente se interpretan, como JavaScript o incluso los scripts Bash, no tienen prácticamente ningún paso de compilación (por lo que minimizan (1) el tiempo cero). Entonces también tienen que decidir entre optimizar (2) y (3). Tradicionalmente, los idiomas interpretados solo cargaron el tiempo de ejecución y luego comenzaron a interpretar el código de la aplicación, pero el advenimiento de JIT (compilación justo a tiempo) lo cambió un poco para mejorar el rendimiento del tiempo de ejecución (3) a expensas de una inicialización potencialmente más larga (2) .

Para responder específicamente a su pregunta: ¿Qué se gana a cambio de ser lento para comenzar? Principalmente rendimiento en tiempo de ejecución. Esto solía ser un problema mayor cuando las computadoras eran mucho más lentas. En el caso de JVM / CLR, la portabilidad también se obtiene (si tiene una JVM, puede ejecutar el mismo código en las arquitecturas x86 y ARM).

Los lenguajes funcionales como Clojure y Haskell tienen problemas algo diferentes que resolver que los lenguajes imperativos mencionados anteriormente. Por ejemplo, Haskell incrusta su propio tiempo de ejecución en cada binario que genera el compilador de Haskell. Esto permite, entre otras cosas, la semántica de evaluación perezosa de Haskell. Por lo tanto, al contrario de lo que he dicho antes, en el diseño funcional del lenguaje, el diseño del lenguaje (y no solo el diseño de compilación / tiempo de ejecución) puede tener un impacto en el tiempo necesario de inicialización del programa.

Sin embargo, si va a escribir un compilador o un intérprete, le recomiendo que comience con un lenguaje imperativo, ya que compilar el código funcional es mucho más difícil en términos conceptuales. Si te gustan los libros, el Libro del Dragón es muy apreciado por muchos.