Compilación de bytecode vs código de máquina


13

¿La compilación que produce un bytecode provisional (como con Java), en lugar de ir "hasta el final" al código de máquina, generalmente implica menos complejidad (y por lo tanto, probablemente tome menos tiempo)?

Respuestas:


22

Sí, compilar a bytecode de Java es más fácil que compilar a código de máquina. Esto se debe en parte a que solo hay un formato al que dirigirse (como menciona Mandrill, aunque esto solo reduce la complejidad del compilador, no el tiempo de compilación), en parte porque la JVM es una máquina mucho más simple y más conveniente para programar que las CPU reales, ya que se ha diseñado en En conjunto con el lenguaje Java, la mayoría de las operaciones de Java se asignan exactamente a una operación de bytecode de una manera muy simple. Otra razón muy importante es que prácticamente noLa optimización tiene lugar. Casi todas las preocupaciones de eficiencia se dejan al compilador JIT (o a la JVM en su conjunto), por lo que todo el extremo medio de los compiladores normales desaparece. Básicamente, puede recorrer el AST una vez y generar secuencias de código de bytes listas para cada nodo. Existe cierta "sobrecarga administrativa" de generar tablas de métodos, grupos constantes, etc., pero eso no es nada en comparación con las complejidades de, digamos, LLVM.


Escribiste "... extremo medio de ...". ¿Quiso decir "... medio a fin de ..."? O tal vez "... parte media de ..."?
Julian A.

66
@Julian "middle end" es un término real, acuñado en analogía con "front end" y "back end" sin tener en cuenta la semántica :)

7

Un compilador es simplemente un programa que toma archivos de texto 1 legibles por humanos y los traduce en instrucciones binarias para una máquina. Si das un paso atrás y piensas en tu pregunta desde esta perspectiva teórica, la complejidad es más o menos la misma. Sin embargo, en un nivel más práctico, los compiladores de código de bytes son más simples.

¿Qué pasos generales tienen que pasar para compilar un programa?

  1. Escaneo, análisis y validación del código fuente.
  2. Convirtiendo la fuente en un árbol de sintaxis abstracta.
  3. Opcional: procese y mejore el AST si la especificación del lenguaje lo permite (por ejemplo, eliminar el código muerto, reordenar operaciones, otras optimizaciones)
  4. Convirtiendo el AST a alguna forma que una máquina entienda.

Solo hay dos diferencias reales entre los dos.

  • En general, un programa con múltiples unidades de compilación requiere vinculación cuando se compila en código máquina y generalmente no con código de bytes. Uno podría dividir si la vinculación es parte de la compilación en el contexto de esta pregunta. Si es así, la compilación de código de bytes sería un poco más simple. Sin embargo, la complejidad de la vinculación se compensa en tiempo de ejecución cuando la VM maneja muchos problemas de vinculación (consulte mi nota a continuación).

  • Los compiladores de código de bytes tienden a no optimizar tanto porque la VM puede hacerlo mejor sobre la marcha (los compiladores JIT son una adición bastante estándar a las VM en la actualidad).

De esto concluyo que los compiladores de código de bytes pueden omitir la complejidad de la mayoría de las optimizaciones y todos los enlaces, difiriendo ambos al tiempo de ejecución de VM. Los compiladores de código de bytes son más simples en la práctica porque incorporan muchas complejidades en la VM que los compiladores de código de máquina toman ellos mismos.

1 Sin contar idiomas esotéricos


3
Ignorar las optimizaciones y tal es una tontería. Estos "pasos opcionales" constituyen una gran parte de la base del código, la complejidad y el tiempo de compilación de la mayoría de los compiladores.

En la práctica, eso es correcto. Estaba creciendo académicamente aquí, actualicé mi respuesta.

¿Existe alguna especificación de lenguaje que realmente prohíba las optimizaciones? ¿Entiendo que algunos idiomas lo dificultan, pero no permiten que ninguno comience?
Davidmh

@Davidmh No conozco ninguna especificación que los prohíba . Tengo entendido que la mayoría dice que el compilador está autorizado, pero no entra en detalles. Cada implementación es diferente porque muchas optimizaciones dependen de los detalles de la CPU, el sistema operativo y la arquitectura de destino en general. Por esta razón, es menos probable que un compilador de código de byte se optimice y, en cambio, se aplique eso a la VM que conoce la arquitectura subyacente.

4

Yo diría que eso simplifica el diseño del compilador, ya que la compilación siempre es Java a código genérico de máquina virtual. Eso también significa que solo necesita compilar el código una vez y se ejecutará en cualquier plataforma (en lugar de tener que compilar en cada máquina). No estoy tan seguro de si el tiempo de compilación será menor porque puede considerar la máquina virtual como una máquina estandarizada.

Por otro lado, cada máquina tendrá que tener la máquina virtual Java cargada para que pueda interpretar el "código de bytes" (que es el código de máquina virtual resultante de la compilación de código Java), traducirlo al código de máquina real y ejecutarlo .

Imo, esto es bueno para programas muy grandes pero muy malo para los pequeños (porque la máquina virtual es un desperdicio de memoria).


Veo. ¿Entonces cree que la complejidad de asignar el código de bytes a la máquina estándar (es decir, la JVM) coincidiría con la de asignar el código fuente a una máquina física, sin dejar ninguna razón para pensar que el código de bytes resultaría en un tiempo de compilación más corto?
Julian A.

Eso no es lo que dije. Dije que asignar el código Java al código de bytes (que es el ensamblador de máquina virtual) coincidiría con el de asignar el código fuente (Java) al código físico de la máquina.
Mandrill

3

La complejidad de la compilación depende en gran medida de la brecha semántica entre el idioma de origen y el idioma de destino y el nivel de optimización que desea aplicar al cerrar esta brecha.

Por ejemplo, compilar el código fuente de Java en el código de bytes JVM es relativamente sencillo, ya que hay un subconjunto central de Java que se asigna casi directamente a un subconjunto de código de bytes JVM. Hay algunas diferencias: Java tiene bucles pero no GOTO, la JVM tiene GOTOpero no bucles, Java tiene genéricos, la JVM no, pero se pueden resolver fácilmente (la transformación de bucles a saltos condicionales es trivial, el borrado de texto es un poco menos así, pero aún manejable). Hay otras diferencias pero menos severas.

Compilar el código fuente de Ruby en el código de bytes JVM es mucho más complicado (especialmente antes invokedynamicy MethodHandlesse introdujeron en Java 7, o más precisamente en la 3a edición de la especificación JVM). En Ruby, los métodos se pueden reemplazar en tiempo de ejecución. En la JVM, la unidad de código más pequeña que se puede reemplazar en tiempo de ejecución es una clase, por lo que los métodos de Ruby deben compilarse no a los métodos JVM sino a las clases JVM. El envío del método Ruby no coincide con el envío del método JVM y antes invokedynamic, no había forma de inyectar su propio mecanismo de envío del método en la JVM. Ruby tiene continuaciones y corutinas, pero la JVM carece de las instalaciones para implementarlas. (Los JVMGOTO está restringido a objetivos de salto dentro del método). El único control primitivo de flujo que tiene la JVM, que sería lo suficientemente poderoso como para implementar continuaciones, son excepciones e implementar hilos de corutina, los cuales son extremadamente pesados, mientras que el propósito de las corutinas es Ser muy ligero.

OTOH, compilar el código fuente de Ruby para el código de byte de Rubinius o el código de byte de YARV es nuevamente trivial, ya que ambos están diseñados explícitamente como un objetivo de compilación para Ruby (aunque Rubinius también se ha utilizado para otros lenguajes como CoffeeScript y Fancy). .

Del mismo modo, compilar código nativo x86 en código de bytes JVM no es sencillo, de nuevo, hay una brecha semántica bastante grande.

Haskell es otro buen ejemplo: con Haskell, hay varios compiladores listos para la producción de alto rendimiento y resistencia industrial que producen código de máquina nativo x86, pero hasta la fecha, no hay un compilador que funcione para la JVM o la CLI, porque la semántica la brecha es tan grande que es muy complejo cerrarla. Entonces, este es un ejemplo en el que la compilación en código máquina nativa es en realidad menos compleja que compilar en código de bytes JVM o CIL. Esto se debe a que el código de máquina nativo tiene primitivas de nivel mucho más bajo ( GOTOpunteros, ...) que pueden ser más fáciles de "coaccionar" para hacer lo que desea que usar primitivas de nivel superior, como llamadas a métodos o excepciones.

Por lo tanto, se podría decir que cuanto más alto sea el idioma de destino, más estrechamente debe coincidir con la semántica del idioma de origen para reducir la complejidad del compilador.


0

En la práctica, la mayoría de las JVM de hoy en día son software muy complejos, que compilan JIT ( por lo que JVM traduce dinámicamente el código de bytes al código de máquina).

Entonces, aunque la compilación del código fuente de Java (o el código fuente de Clojure) al código de bytes JVM es de hecho más simple, la propia JVM está haciendo una traducción compleja al código de la máquina.

El hecho de que esta traducción JIT dentro de JVM sea dinámica le permite a JVM enfocarse en las partes más relevantes del código de bytes. Hablando en términos prácticos, la mayoría de JVM optimiza más las partes más populares (por ejemplo, los métodos más llamados o los bloques básicos más ejecutados) del código de bytes JVM.

No estoy seguro de que la complejidad combinada de JVM + Java para compilar bytecode sea significativamente menor que la complejidad de los compiladores anticipados.

Tenga en cuenta también que los compiladores más tradicionales (como GCC o Clang / LLVM ) están transformando el código fuente de entrada C (o C ++, o Ada, ...) en una representación interna ( Gimple para GCC, LLVM para Clang) que es bastante similar a Algunos bytecode. Luego están transformando esas representaciones internas (primero optimizándolas en sí mismas, es decir, la mayoría de los pases de optimizaciones de GCC toman Gimple como entrada y producen Gimple como salida; luego emiten código ensamblador o de máquina) a código objeto.

Por cierto, con la infraestructura reciente de GCC (notablemente libgccjit ) y LLVM, puede usarlos para compilar algún otro lenguaje (o el suyo propio) en sus representaciones internas de Gimple o LLVM, y luego beneficiarse de las muchas capacidades de optimización de la gama media y posterior. partes finales de estos compiladores.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.