Pregunta

Ok, primero, no quiero ningún tipo de Flamewar aquí ni nada como él. Mi pregunta más importante es más teórica e incluirá pocos ejemplos.

Entonces, como escribí, no puedo entender cómo el lenguaje interpretado puede ser incluso poco eficiente. Y desde que es moderno, tomaré Java como ejemplo.

Volvamos a los días donde no había compiladores JIT. Java tiene su máquina virtual, que es básicamente su hardware. Usted escribe código, que se compiló en Bytecode para quitar al menos algún trabajo de la máquina virtual, está bien. Pero teniendo en cuenta cuán complejo, incluso el conjunto de instrucciones RISC puede ser en el hardware, ni siquiera puedo pensar en la forma de hacerlo en el hardware emulado del software.

No tengo experiencia en escribir máquinas virtuales, por lo que no sé cómo se hace al nivel más eficiente, pero no puedo pensar en nada más eficiente que probar cada instrucción para coincidir con las acciones apropiadas. Ya sabes, algo como: if(instruction=="something") { (do it) } else if(instruction=="something_diffrent"){ (do it) }etc....

Pero esto tiene que ser terriblemente lento. Y aún así, incluso hay artículos que Java era lento antes de los compiladores de JIT, todavía dicen que no es tan lento. Pero para emularlo debe tomar muchos ciclos de reloj de HW real para realizar una instrucción Bytecode.

Y aún así, incluso las plataformas enteras se basan en Java. Por ejemplo, Android. Y los primeros veros de Android no tenían compilador JIT. Fueron interpretados. ¿Pero no debería ser Android terriblemente lento? Y sin embargo no lo es. Sé, cuando llamas a alguna función API, desde la biblioteca de Android, están escritas en código de máquina, por lo que son eficientes, por lo que esto ayuda mucho.

Pero imagine que escribirías tu propio motor de juego desde Sratch, usando API solo para mostrar imágenes. Debería hacer muchas operaciones de copia de matriz, muchos cálculos que serían terriblemente lentos cuando se emulan.

Y ahora algunos ejemplos como prometí. Como trabajo principalmente con MCU, encontré JVM para Atmel AVR MCU. Afirma que 8MHz MCU puede hacer 20k Java Optcodes por segundo. Pero dado que AVR puede hacer la mayoría de las instrucciones en uno o dos ciclos, digamos el promedio de instrucciones 6000000. Esto nos da que JVM sin compilador JIT es 300 veces más lento al código de la máquina. Entonces, ¿por qué convertirse en Java tan popular sin JIT Compiler? ¿No es esta mala pérdida de rendimiento? Simplemente no puedo entenderlo. Gracias.

¿Fue útil?

Solución

Hemos tenido código de byte alrededor durante mucho tiempo. En el antiguo Apple II, el sistema P USCD era muy popular, que compilaba Pascal en el código de byte, que sería interpretado por un 6502 de 8 bits que podría estar funcionando a 2 MHz. Esos programas se ejecutaron razonablemente rápido.

Un intérprete de bytecode generalmente se basaría en una mesa de salto en lugar de una cadena de if/then/else declaraciones. En C o C ++, esto implicaría un switch declaración. Fundamentalmente, el intérprete tendría el equivalente de una matriz de código de procesamiento y usaría el código de operación en la instrucción del código de byte como índice de la matriz.

También es posible tener un código de byte que sea de nivel superior que las instrucciones de la máquina, de modo que una instrucción de código de byte se traduciría en varias instrucciones de código de máquina, a veces numerosas. Un código de byte que se construyó para un lenguaje en particular puede hacer esto con bastante facilidad, ya que solo tiene que coincidir con el control y las estructuras de datos de ese lenguaje en particular. Esto estira la interpretación sobre la cabeza y hace que el intérprete sea más eficiente.

Es probable que un lenguaje interpretado tenga cierta penalización de velocidad en comparación con un lenguaje compilado, pero esto a menudo no es importante. Muchos programas procesan la entrada y salida a velocidad humana, y eso deja una tremenda cantidad de rendimiento que se puede desperdiciar. Es probable que incluso un programa vinculado a la red tenga mucha más energía de la CPU disponible de la que necesita. Hay programas que pueden usar toda la eficiencia de la CPU que pueden obtener, y por razones obvias tienden a no ser escritas en idiomas interpretados.

Y, por supuesto, existe la cuestión de lo que se obtiene por alguna ineficiencia que pueda o no marcar la diferencia. Las implementaciones de lenguaje interpretadas tienden a ser más fáciles de portar que las implementaciones compiladas, y el código de byte real a menudo es portátil. Puede ser más fácil poner funcionalidad de nivel superior en el idioma. Permite que el paso de compilación sea mucho más corto, lo que significa que la ejecución puede comenzar mucho más rápido. Puede permitir mejores diagnósticos si algo sale mal.

Otros consejos

¿Pero no debería ser entonces Android terriblemente lento?

Defina "terriblemente lento". Es un teléfono. Tiene que procesar "marcar el primer dígito" antes de marcar el segundo dígito.

En cualquier aplicación interactiva, el factor limitante es siempre el tiempo de reacción humana. Podría pasar 100 veces más lento y aún ser más rápido que el usuario.

Entonces, para responder a su pregunta, sí, los intérpretes son lentos, pero generalmente son lo suficientemente rápidos, particularmente a medida que el hardware sigue siendo más rápido.

Recuerde que cuando se introdujo Java, se vendió como un lenguaje de applet de web (reemplazando y ahora reemplazado por JavaScript --- que también se interpretó). Fue solo después de la compilación JIT que se hizo popular en los servidores.

Los intérpretes de Bytecode pueden ser más rápidos que una línea de if () s utilizando una tabla de salto:

 void (*jmp_tbl)[256] = ...;  /* array of function pointers */
 byte op = *program_counter++;
 jmp_tbl[op]();

Hay dos formas diferentes de abordar esta pregunta.

(i) "¿Por qué está bien ejecutar código lento"

Como James ya mencionó anteriormente, a veces la velocidad de ejecución no es todo lo que le interesa. Para muchas aplicaciones que se ejecutan en modo interpretado pueden ser "lo suficientemente rápidos". Debe tener en cuenta cómo se utilizará el código que está escribiendo.

(ii) "¿Por qué el código interpretado es inneficiente"?

Hay muchas maneras en que puede implementar un intérprete. En su pregunta sobre el que habla el enfoque más ingenuo: básicamente un gran interruptor, interpretando cada instrucción JVM tal como se lee.

Pero puede optimizar eso: por ejemplo, en lugar de mirar una sola instrucción JVM, puede ver una secuencia de ellos y buscar patrones para los cuales tiene interpretaciones más eficientes disponibles. JVM de Sun en realidad hace algunas de estas optimizaciones en el intérprete. En un trabajo anterior, un tipo se tomó un tiempo para optimizar al intérprete de esa manera e interpretó que Java Bytecode estaba funcionando notablemente más rápido después de sus cambios.

Pero en los JVM modernos que contienen un compilador JIT, el intérprete es solo un trampolín hasta que el JIT hace su trabajo, por lo que la gente realmente no pasa tanto tiempo optimizando al intérprete.

12 MHz sería un attiny, que es un microprocesador de 8 bits. Eso significa (por ejemplo) que una instrucción nativa de "agregar" solo puede agregar dos números de 8 bits juntos para obtener un resultado de 9 bits. El JVM es básicamente un procesador virtual de 32 bits. Eso significa que su instrucción ADD agrega dos 32- Números de bit juntos para producir un resultado de 33 bits.

Como tal, cuando compare las tasas de instrucciones, debe esperar una reducción 4: 1 en la tasa de instrucción como un mínimo absoluto. En realidad, si bien es fácil simular un ADD de 32 bits con 4 adiciones de 8 bits (con acarreos), algunas cosas no se escalan así. Solo por ejemplo, según el propio Atmel nota de la aplicación, una multiplicación de 16x16 que produce un resultado de 32 bits se ejecuta en ~ 218 ciclos de reloj. La misma nota de aplicación muestra una división de 16/16 bits (que produce un resultado de 8 bits) que se ejecuta en 255 ciclos.

Suponiendo que las escamas linealmente, podemos esperar que las versiones de 32 bits de la multiplicación tomen ~ 425-450 ciclos de reloj, y la división ~ 510 ciclos. En realidad, probablemente deberíamos esperar un poco de sobrecarga, lo que reduciría aún más la velocidad, agregar al menos un 10% a esas estimaciones probablemente los hace más realistas.

En pocas palabras: cuando compara manzanas con manzanas, se hace evidente que una gran cantidad de la diferencia de velocidad que está hablando no es real en absoluto (o no es atribuible JVM Overhead de todos modos).

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top