6

Voy a aprender Ruby. Sé que es un lenguaje interpretado. Sé que los lenguajes compilados se traducen eventualmente al código de máquina, pero ¿qué hace el intérprete de rubíes? Leí que el intérprete estaba escrito en C, pero ¿cada línea de rubí se convierte a c, que de nuevo se compila en código de máquina? También escuché sobre JIT, pero si eso agrega mucha complejidad a la respuesta, no es necesario que responda eso. Lo que estoy buscando es lo que le sucede a mi código de Ruby.¿Cómo se ejecutan los idiomas interpretados (como Ruby)?

Respuesta

8

Convierte el código de Ruby en una forma de representación "intermedia" más simple (en versiones recientes, compila en bytecode). También construye, en la memoria de su computadora, una máquina virtual que simula una máquina física que ejecuta esa representación.

Esta máquina refleja una física, al menos en la medida razonable y útil. Con frecuencia tiene una memoria para instrucciones, un contador de programa, una pila para almacenar valores intermedios y direcciones de retorno, etc. Algunas máquinas más sofisticadas también tienen registros. Hay un conjunto de instrucciones fijo y relativamente primitivo (en comparación con lanugages como Ruby, en comparación con los conjuntos de instrucciones de CPU reales). Al igual que una CPU, la máquina virtual realiza bucles sin fin:

  • Lea la instrucción actual (identificada por el contador del programa).
  • (lo decodifica, aunque esto suele ser mucho más simple que en las CPU reales, al menos que las CISC.)
  • Lo ejecuta (manipulando la pila y/o registrando en el proceso).
  • Actualiza el contador del programa.

Con un intérprete, todo esto sucede a través de una capa de indirección. Su CPU física real no tiene idea de lo que está haciendo. La VM es el software en sí, cada uno de los pasos anteriores es delegado a la CPU en varios (en casos con instrucciones de bytecode de alto nivel, posiblemente docenas o cientos) de ciclos de CPU físicos. Y esto sucede cada vez que se lee una instrucción.

Ingrese la compilación JIT. La forma más simple simplemente reemplaza cada instrucción bytecode con una copia (algo optimizada) del código que se ejecutará cuando el intérprete lo encuentre. Esto ya otorga una ganancia de velocidad, p. la manipulación del contador del programa se puede omitir. Pero hay variantes incluso más inteligentes.

Los JIT de rastreo, por ejemplo, comienzan como un intérprete regular, y además observan el programa que ejecutan. Si notan que el programa pasa mucho tiempo en una sección particular del código (casi siempre, un bucle o una función llamada desde bucles), comienza a registrar lo que hace durante esto: genera un rastreo. Cuando alcanza el punto donde comenzó a grabar (después de una iteración del ciclo), lo llama un día y compila la traza con el código de la máquina. Pero dado que vio cómo el programa realmente se comporta en tiempo de ejecución, puede generar código que se ajusta exactamente a este comportamiento. Tomemos por ejemplo un bucle añadiendo enteros. El código de máquina no contendrá ninguna de las comprobaciones de tipos y llamadas a funciones que realmente realiza el intérprete. Al menos, no contendrá la mayoría de ellos. Es , para garantizar la corrección, agregue comprobaciones de que las condiciones bajo las cuales se registró la traza (por ejemplo, las variables involucradas son números enteros) todavía se mantienen. Cuando falla la verificación, se desactiva y se reanuda la interpretación hasta que se registre otra. Pero hasta que eso ocurra, podría haber realizado cien iteraciones a una velocidad que rivaliza con el código C escrito a mano.

Cuestiones relacionadas