ejemplos compilador java jit

compilador - bytecode java ejemplos



¿Por qué es Java más rápido cuando se usa un JIT en lugar de compilar en un código de máquina? (10)

He oído que Java debe usar un JIT para ser rápido. Esto tiene mucho sentido cuando se compara con la interpretación, pero ¿por qué alguien no puede hacer un compilador adelantado que genere código Java rápido? Sé sobre gcj , pero no creo que su salida sea típicamente más rápida que Hotspot, por ejemplo.

¿Hay cosas sobre el lenguaje que hacen esto difícil? Creo que todo se reduce a estas cosas:

  • Reflexión
  • Carga de clases

¿Qué me estoy perdiendo? Si evito estas características, ¿sería posible compilar el código Java una vez al código de máquina nativo y terminar?


Al final, se reduce al hecho de que tener más información permite mejores optimizaciones. En este caso, el JIT tiene más información sobre la máquina real en la que se está ejecutando el código (como mencionó Andrew) y también tiene mucha información de tiempo de ejecución que no está disponible durante la compilación.


Creo que el hecho de que el compilador oficial de Java sea un compilador JIT es una gran parte de esto. ¿Cuánto tiempo se ha pasado optimizando la JVM frente a un compilador de código de máquina para Java?


Dimitry Leskov está absolutamente aquí.

Todo lo anterior es solo una teoría de lo que podría hacer JIT más rápido, la implementación de cada scenaro es casi imposible. Además, debido al hecho de que solo tenemos un puñado de conjuntos de instrucciones diferentes en las CPU x86_64, hay muy poco que ganar al apuntar a cada conjunto de instrucciones en la CPU actual. Siempre me guío por la regla de apuntar a x86_64 y SSE4.2 al crear aplicaciones de rendimiento crítico en código nativo. La estructura fundamental de Java está causando una tonelada de limitaciones, JNI puede ayudarlo a mostrar cuán ineficiente es, JIT solo lo endulza al hacerlo más rápido en general. Además del hecho de que cada función por defecto es virtual, también usa tipos de clase en tiempo de ejecución en lugar de, por ejemplo, C ++. C ++ tiene una gran ventaja aquí cuando se trata de rendimiento, ya que no se requiere que se cargue ningún objeto de clase en tiempo de ejecución, todos los bloques de datos se asignan en la memoria y solo se inicializan cuando se solicitan. En otras palabras, C ++ no tiene tipos de clase en tiempo de ejecución. Las clases de Java son objetos reales, no solo plantillas. No voy a entrar en GC porque eso es irrelevante. Las cadenas Java también son más lentas porque utilizan la agrupación dinámica de cadenas, lo que requeriría que el tiempo de ejecución realice búsquedas de cadenas en la tabla de agrupaciones cada vez. Muchas de esas cosas se deben al hecho de que Java no se creó primero para ser rápido, por lo que su fundamento siempre será lento. La mayoría de los idiomas nativos (principalmente C / C ++) se crearon específicamente para ser lean y mean, sin pérdida de memoria o recursos. Las primeras versiones de Java, de hecho, fueron terriblemente lentas y desperdiciadas en la memoria, con muchos metadatos innecesarios para las variables y cuáles no. Tal como está hoy, JIT es capaz de producir código más rápido que los lenguajes AOT seguirá siendo una teoría.

Piense en todo el trabajo que necesita el JIT para realizar el seguimiento del JIT lento, incremente un contador cada vez que se llame a una función, verifique cuántas veces se ha llamado ... etc., etc. Ejecutar el JIT está tomando mucho tiempo. El comercio de mis ojos no vale la pena. Esto es solo para PC

¿Alguna vez ha intentado ejecutar Java en Raspberry y otros dispositivos integrados? Absolutamente terrible rendimiento. ¿JavaFX en Raspberry? Ni siquiera funcional ... Java y su JIT están muy lejos de cumplir con todo lo que anuncia y la teoría que la gente expone ciegamente.


El compilador JIT de Java también es perezoso y adaptable.

Perezoso

Al ser perezoso, solo compila métodos cuando llega a ellos en lugar de compilar todo el programa (muy útil si no usas parte de un programa). La carga de clases en realidad ayuda a que el JIT sea más rápido al permitirle ignorar las clases que aún no ha encontrado.

Adaptado

Al ser adaptable, primero emite una versión rápida y sucia del código de la máquina y luego solo retrocede y realiza un trabajo completo si ese método se usa con frecuencia.


El verdadero asesino para cualquier compilador AOT es:

Class.forName(...)

Esto significa que no puede escribir un compilador AOT que cubra TODOS los programas de Java ya que hay información disponible solo en tiempo de ejecución sobre las características del programa. Sin embargo, puedes hacerlo en un subconjunto de Java, que es lo que creo que hace gcj.

Otro ejemplo típico es la capacidad de un JIT para métodos en línea como getX () directamente en los métodos de llamada si se encuentra que es seguro hacerlo, y deshacerlo si es apropiado, incluso si el programador no lo ayuda explícitamente diciendo que Un método es definitivo. El JIT puede ver que en el programa en ejecución un método dado no se anula y, por lo tanto, en este caso se puede tratar como final. Esto podría ser diferente en la siguiente invocación.


En teoría, un compilador JIT tiene una ventaja sobre AOT si tiene suficiente tiempo y recursos computacionales disponibles . Por ejemplo, si tiene una aplicación empresarial que se ejecuta durante días y meses en un servidor multiprocesador con suficiente memoria RAM, el compilador JIT puede producir un mejor código que cualquier compilador AOT.

Ahora, si tiene una aplicación de escritorio, las cosas como el inicio rápido y el tiempo de respuesta inicial (donde AOT brilla) se vuelven más importantes, además de que la computadora no tenga los recursos suficientes para las optimizaciones más avanzadas.

Y si tiene un sistema integrado con recursos escasos, JIT no tiene ninguna posibilidad contra AOT.

Sin embargo, lo anterior era toda la teoría. En la práctica, crear un compilador JIT tan avanzado es mucho más complicado que uno AOT decente. ¿Qué tal alguna evidencia práctica ?


La capacidad de Java para integrarse a través de los límites de los métodos virtuales y realizar un envío eficiente de la interfaz requiere un análisis en tiempo de ejecución antes de compilar, en otras palabras, requiere un JIT. Dado que todos los métodos son virtuales y las interfaces se utilizan "en todas partes", hace una gran diferencia.


Los JIT pueden identificar y eliminar algunas condiciones que solo se pueden conocer en tiempo de ejecución. Un buen ejemplo es la eliminación de las llamadas virtuales que usan las máquinas virtuales modernas, por ejemplo, cuando la JVM encuentra una instrucción invokevirtual o invokeinterface , si solo se ha cargado una clase que invalida el método invocado, la máquina virtual puede hacer que la llamada virtual sea estática y, por lo tanto, puede para integrarlo. Para un programa en C, por otra parte, un puntero de función es siempre un puntero de función, y una llamada a él no puede estar en línea (en el caso general, de todos modos).

Aquí hay una situación en la que la JVM puede integrar una llamada virtual:

interface I { I INSTANCE = Boolean.getBoolean("someCondition")? new A() : new B(); void doIt(); } class A implements I { void doIt(){ ... } } class B implements I { void doIt(){ ... } } // later... I.INSTANCE.doIt();

Suponiendo que no creamos instancias A o B otro lugar y que someCondition se establecen en true , la JVM sabe que la llamada a doIt() siempre significa A.doIt , y por lo tanto puede evitar la búsqueda en la tabla de métodos, y luego alinear llamada. Una construcción similar en un entorno no JIT no sería inlinable.


Pegaré una respuesta interesante dada por James Gosling en el libro Masterminds of Programming .

Bueno, he oído decir que efectivamente tienes dos compiladores en el mundo de Java. Tienes el compilador para el código de bytes de Java, y luego tienes tu JIT, que básicamente compila todo específicamente de nuevo. Todas tus optimizaciones de miedo están en el JIT .

James: Exactamente. Estos días estamos superando a los muy buenos compiladores de C y C ++ casi siempre. Cuando va al compilador dinámico, obtiene dos ventajas cuando el compilador se ejecuta en el último momento. Una es que sabes exactamente en qué chipset estás corriendo. Muchas veces cuando las personas compilan un fragmento de código C, tienen que compilarlo para ejecutarse en el tipo de arquitectura x86 genérica. Casi ninguno de los binarios que obtienes está particularmente bien sintonizado para ninguno de ellos. Descarga la última copia de Mozilla y se ejecutará en casi cualquier CPU de arquitectura Intel. Hay prácticamente un binario de Linux. Es bastante genérico y está compilado con GCC, que no es un compilador de C muy bueno.

Cuando se ejecuta HotSpot, sabe exactamente qué chipset está ejecutando. Sabe exactamente cómo funciona el caché. Sabe exactamente cómo funciona la jerarquía de memoria. Sabe exactamente cómo funcionan todos los interbloqueos de tuberías en la CPU. Sabe qué extensiones de conjunto de instrucciones tiene este chip. Optimiza precisamente para qué máquina estás. Luego, la otra mitad es que realmente ve la aplicación mientras se está ejecutando. Es capaz de tener estadísticas que saben qué cosas son importantes. Es capaz de alinear cosas que un compilador de C nunca podría hacer. El tipo de cosas que se integran en el mundo de Java es bastante sorprendente. Luego se agrega a la forma en que funciona la gestión de almacenamiento con los recolectores de basura modernos. Con un recolector de basura moderno, la asignación de almacenamiento es extremadamente rápida.


Un compilador JIT puede ser más rápido porque el código de la máquina se genera en la máquina exacta en la que también se ejecutará. Esto significa que el JIT tiene la mejor información posible disponible para emitir código optimizado.

Si precompila el código de bytes en el código de la máquina, el compilador no puede optimizar para las máquinas de destino, solo la máquina de compilación.