usar icon codigo adjust java compilation interpreter bytecode interpreted-language

icon - stretch image java jlabel



¿Cómo interpreta el código un intérprete? (4)

Para simplificar, imagine este escenario, tenemos una computadora de 2 bits, que tiene un par de registros de 2 bits llamados r1 y r2 y solo funciona con direccionamiento inmediato.

Digamos que la secuencia de bits 00 significa agregar a nuestra CPU. También 01 significa mover datos a r1 y 10 significa mover datos a r2.

Así que hay un lenguaje ensamblador para esta computadora y un ensamblador, donde se escribiría un código de ejemplo como

mov r1,1 mov r2,2 add r1,r2

Simplemente, cuando armo este código a un idioma nativo y el archivo será algo así como:

0101 1010 0001

Los 12 bits anteriores es el código nativo para:

Put decimal 1 to R1, Put decimal 2 to R2, Add the data and store in R1.

Así que esto es básicamente cómo funciona un código compilado, ¿verdad?

Digamos que alguien implementa una JVM para esta arquitectura. En Java escribiré código como:

int x = 1 + 2;

¿Cómo exactamente JVM interpretará este código? Me refiero a que finalmente se debe pasar el mismo patrón de bits a la CPU, ¿no es así? Todos los cpu tienen una serie de instrucciones que puede comprender y ejecutar, y, después de todo, son solo algunos bits. Digamos que el código de bytes compilado de Java se ve algo así:

1111 1100 1001

o lo que sea .. ¿Significa que la interpretación cambia este código a 0101 1010 0001 cuando se ejecuta? Si es así, ya está en el Código nativo, entonces ¿por qué se dice que JIT solo se activa después de varias veces? Si no lo convierte exactamente a 0101 1010 0001, ¿qué hace? ¿Cómo hace el cpu hacer la adición?

Tal vez hay algunos errores en mis suposiciones.

Sé que la interpretación es lenta, el código compilado es más rápido pero no portátil, y una máquina virtual "interpreta" un código, pero ¿cómo? Estoy buscando "cómo se realiza la interpretación exacta / técnica". Cualquier puntero (como libros o páginas web) también es bienvenido en lugar de respuestas.


Desafortunadamente, la arquitectura de CPU que describe es demasiado restringida para que esto quede realmente claro con todos los pasos intermedios. En su lugar, escribiré pseudo-C y pseudo-x86-assembler, con suerte de una manera clara sin estar muy familiarizado con C o x86.

El código de bytes de JVM compilado podría tener este aspecto:

ldc 0 # push first first constant (== 1) ldc 1 # push the second constant (== 2) iadd # pop two integers and push their sum istore_0 # pop result and store in local variable

El intérprete tiene (una codificación binaria de) estas instrucciones en una matriz, y un índice que se refiere a la instrucción actual. También tiene una matriz de constantes y una región de memoria utilizada como pila y una para variables locales. Entonces el bucle de intérprete se ve así:

while (true) { switch(instructions[pc]) { case LDC: sp += 1; // make space for constant stack[sp] = constants[instructions[pc+1]]; pc += 2; // two-byte instruction case IADD: stack[sp-1] += stack[sp]; // add to first operand sp -= 1; // pop other operand pc += 1; // one-byte instruction case ISTORE_0: locals[0] = stack[sp]; sp -= 1; // pop pc += 1; // one-byte instruction // ... other cases ... } }

Este código C se compila en código de máquina y se ejecuta. Como puede ver, es altamente dinámico: inspecciona cada instrucción de código de bytes cada vez que se ejecuta esa instrucción, y todos los valores pasan por la pila (es decir, la RAM).

Si bien la adición real en sí misma probablemente ocurre en un registro, el código que rodea a la adición es bastante diferente de lo que emitiría un compilador de código de Java a máquina. Aquí hay un extracto de lo que un compilador de C podría convertir lo anterior en (pseudo-x86):

.ldc: incl %esi # increment the variable pc, first half of pc += 2; movb %ecx, program(%esi) # load byte after instruction movl %eax, constants(,%ebx,4) # load constant from pool incl %edi # increment sp movl %eax, stack(,%edi,4) # write constant onto stack incl %esi # other half of pc += 2 jmp .EndOfSwitch .addi movl %eax, stack(,%edi,4) # load first operand decl %edi # sp -= 1; addl stack(,%edi,4), %eax # add incl %esi # pc += 1; jmp .EndOfSwitch

Puede ver que los operandos para la adición provienen de la memoria en lugar de estar codificados, aunque a los efectos del programa Java son constantes. Eso es porque para el intérprete , no son constantes. El intérprete se compila una vez y luego debe poder ejecutar todo tipo de programas, sin generar código especializado.

El propósito del compilador JIT es hacer precisamente eso: generar código especializado. Un JIT puede analizar las formas en que se utiliza la pila para transferir datos, los valores reales de varias constantes en el programa y la secuencia de cálculos realizados, para generar un código que haga lo mismo de manera más eficiente. En nuestro programa de ejemplo, asignaría la variable local 0 a un registro, reemplazaría el acceso a la tabla constante con constantes en movimiento en los registros ( movl %eax, $1 ) y redirigiría los accesos de la pila a los registros de la máquina correcta. Ignorando algunas optimizaciones más (propagación de la copia, plegado constante y eliminación del código muerto) que normalmente se realizarían, podría terminar con un código como el siguiente:

movl %ebx, $1 # ldc 0 movl %ecx, $2 # ldc 1 movl %eax, %ebx # (1/2) addi addl %eax, %ecx # (2/2) addi # no istore_0, local variable 0 == %eax, so we''re done


No todas las computadoras tienen el mismo conjunto de instrucciones. Java bytecode es un tipo de esperanto, un lenguaje artificial para mejorar la comunicación. La máquina virtual Java convierte el bytecode universal de Java al conjunto de instrucciones de la computadora en la que se ejecuta.

Entonces, ¿cómo figura JIT aquí? El propósito principal del compilador JIT es la optimización. A menudo hay diferentes maneras de traducir un determinado código de byte en el código de la máquina de destino. La traducción con mayor rendimiento ideal a menudo no es obvia porque puede depender de los datos. También hay límites sobre hasta qué punto un programa puede analizar un algoritmo sin ejecutarlo: el problema de la detención es una limitación bien conocida, pero no la única. Entonces, lo que hace el compilador JIT es probar diferentes traducciones posibles y medir qué tan rápido se ejecutan con los datos reales que procesa el programa. Así que se requieren varias ejecuciones hasta que el compilador JIT encuentre la traducción perfecta.


Simplificando, el intérprete es un bucle infinito con un interruptor gigante dentro. Lee el código de bytes de Java (o alguna representación interna) y emula una CPU que lo ejecuta. De esta manera, la CPU real ejecuta el código del intérprete, que emula a la CPU virtual. Esto es dolorosamente lento. La instrucción virtual única que agrega dos números requiere tres llamadas de función y muchas otras operaciones. Una sola instrucción virtual toma un par de instrucciones reales para ejecutar. Esto también es menos eficiente en memoria ya que tiene pila, registros e indicadores de instrucción emulados, tanto reales como emulados.

while(true) { Operation op = methodByteCode.get(instructionPointer); switch(op) { case ADD: stack.pushInt(stack.popInt() + stack.popInt()) instructionPointer++; break; case STORE: memory.set(stack.popInt(), stack.popInt()) instructionPointer++; break; ... } }

Cuando se interpreta un método varias veces, se activa el compilador JIT. Leerá todas las instrucciones virtuales y generará una o más instrucciones nativas que hacen lo mismo. Aquí estoy generando una cadena con ensamblaje de texto que requeriría un ensamblaje adicional a las conversiones binarias nativas.

for(Operation op : methodByteCode) { switch(op) { case ADD: compiledCode += "popi r1" compiledCode += "popi r2" compiledCode += "addi r1, r2, r3" compiledCode += "pushi r3" break; case STORE: compiledCode += "popi r1" compiledCode += "storei r1" break; ... } }

Después de generar el código nativo, JVM lo copiará en algún lugar, marcará esta región como ejecutable e indicará al intérprete que lo invoque en lugar de interpretar el código de bytes la próxima vez que se invoque este método. La instrucción virtual individual aún podría tomar más de una instrucción nativa, pero esta será casi tan rápida como la compilación anticipada del código nativo (como en C o C ++). La compilación suele ser mucho más lenta que la interpretación, pero se debe hacer solo una vez y solo para los métodos elegidos.


Uno de los pasos importantes en Java es que el compilador primero traduce el código .class en un archivo .class , que contiene el código de .java Java. Esto es útil, ya que puede tomar archivos .class y ejecutarlos en cualquier máquina que entienda este lenguaje intermedio , al traducirlo en el lugar línea por línea, o por partes. Esta es una de las funciones más importantes del compilador + intérprete java. Puede compilar directamente el código fuente de Java a binario nativo, pero esto niega la idea de escribir el código original una vez y poder ejecutarlo en cualquier lugar. Esto se debe a que el código binario nativo compilado solo se ejecutará en la misma arquitectura de hardware / sistema operativo para la que se compiló. Si desea ejecutarlo en otra arquitectura, tendría que volver a compilar la fuente en esa. Con la compilación al código de bytes de nivel intermedio, no es necesario arrastrar alrededor del código fuente, sino del código de bytes. Es un problema diferente, ya que ahora necesita una JVM que pueda interpretar y ejecutar el bytecode. Como tal, la compilación al bytecode de nivel intermedio, que luego ejecuta el intérprete, es una parte integral del proceso.

En cuanto a la ejecución real del código en tiempo real: sí, la JVM eventualmente interpretará / ejecutará algún código binario que puede o no ser idéntico al código compilado de forma nativa. Y en un ejemplo de una línea, pueden parecer superficialmente iguales. Pero la interpretación no suele precompilar todo, sino que pasa por el código de bytes y se traduce a línea por línea binaria o parte por parte. Hay ventajas y desventajas en esto (en comparación con el código compilado de forma nativa, por ejemplo, compiladores C y C) y muchos recursos en línea para leer más. Vea mi respuesta here , o this , o this .