Java: ¿cuánto tiempo usa un ciclo vacío?

Estoy tratando de probar la velocidad de autoboxing y unboxing en Java, pero cuando trato de compararlo con un loop vacío en un primitivo, noté una cosa curiosa. Este fragmento:

for (int j = 0; j < 10; j++) { long t = System.currentTimeMillis(); for (int i = 0; i < 10000000; i++) ; t = System.currentTimeMillis() - t; System.out.print(t + " "); } 

Cada vez que ejecuto esto, devuelve el mismo resultado:

6 7 0 0 0 0 0 0 0 0

¿Por qué los primeros dos bucles siempre toman algo de tiempo, entonces el rest simplemente parece saltearse por el sistema?

En esta respuesta a esta publicación, se dice que la comstackción Just-In-Time será capaz de optimizar esto. Pero si es así, ¿por qué los dos primeros bucles aún tardaron un tiempo?

JIT desencadena DESPUÉS de que se haya ejecutado una cierta pieza de código muchas veces.

HotSpot JVM intentará identificar los “puntos calientes” en su código. Los puntos conflictivos son partes de su código que se ejecutan muchas veces. Para hacer esto, la JVM “contará” las ejecuciones de varias instrucciones, y cuando determine que una determinada pieza se ejecuta con frecuencia, activará el JIT. (Esta es una aproximación, pero es fácil de entender explicada de esta manera).

El JIT (Just-In-Time) toma ese fragmento de código e intenta hacerlo más rápido.

Las técnicas utilizadas por el JIT para hacer que su código corra más rápido son muchas, pero la que más comúnmente crea confusión es:

  1. Tratará de determinar si ese fragmento de código usa variables que no se usan en ningún otro lado (variables inútiles) y las elimina.
  2. Si adquiere y libera el mismo locking varias veces (como llamar a métodos sincronizados del mismo objeto), puede adquirir el locking una vez y hacer todas las llamadas en un solo bloque sincronizado.
  3. Si accede a los miembros de un objeto que no se declaran volátiles, puede decidir optimizarlo (colocando valores en los registros y similares), creando resultados extraños en el código de subprocesos múltiples.
  4. Insertará métodos para evitar el costo de la llamada.
  5. Se traducirá bytecode a código de máquina.
  6. Si el ciclo es completamente inútil, podría eliminarse por completo.

Por lo tanto, la respuesta correcta a su pregunta es que un bucle vacío, después de ser JITed, no tarda en ejecutarse … lo más probable es que ya no exista.

Una vez más, hay muchas otras optimizaciones, pero en mi experiencia, estas se encuentran entre las que han creado más dolores de cabeza.

Además, JIT se está mejorando en cualquier versión nueva de Java, y algunas veces es incluso un poco diferente dependiendo de la plataforma (ya que, en cierta medida, es específica de la plataforma). Las optimizaciones hechas por el JIT son difíciles de entender, porque generalmente no se pueden encontrar usando javap e inspeccionando bytecode, incluso si en versiones recientes de Java algunas de estas optimizaciones se han movido al comstackdor directamente (por ejemplo, desde Java 6 el comstackdor es capaz de detectar y advertir sobre variables locales no utilizadas y métodos privados).

Si está escribiendo algunos bucles para probar algo, generalmente es una buena práctica tener el bucle dentro de un método, llamar al método varias veces ANTES de medir el tiempo, darle una vuelta de “aceleración” y luego realizar el ciclo cronometrado.

Esto generalmente desencadena el JIT en un progtwig simple como el tuyo, incluso si no hay garantía de que realmente se active (o que incluso exista en una plataforma determinada).

Si quieres ponerte paranoico sobre el JIT o el tiempo sin JIT (lo hice): realiza una primera ronda, cronometrando cada ejecución del ciclo, y espera hasta que el tiempo se estabilice (por ejemplo, la diferencia del promedio es inferior al 10%), luego Comience con su tiempo “real”.

El JIT no entra en juego en un trozo de código hasta que determina que hay algún beneficio al hacerlo. Eso significa que los primeros pasos a través de algún código no serán JIT.