¿Cómo escribo un micro-benchmark correcto en Java?

¿Cómo se escribe (y se ejecuta) un micro-benchmark correcto en Java?

Estoy buscando ejemplos de código y comentarios que ilustren varias cosas en las que pensar.

Ejemplo: ¿El punto de referencia debería medir el tiempo / la iteración o las iteraciones / tiempo, y por qué?

Relacionado: ¿Es aceptable el benchmarking de cronómetro?

Consejos para escribir micro benchmarks de los creadores de Java HotSpot :

Regla 0: lea un documento acreditado sobre JVM y micro-benchmarking. Uno bueno es Brian Goetz, 2005 . No esperes demasiado de los micro-puntos de referencia; miden solo un rango limitado de características de rendimiento JVM.

Regla 1: Siempre incluya una fase de calentamiento que ejecute su kernel de prueba hasta el final, suficiente para desencadenar todas las inicializaciones y comstackciones antes de cronometrar la (s) fase (s). (Menos iteraciones está bien en la fase de calentamiento. La regla de oro es varias decenas de miles de iteraciones de bucle interno.)

Regla 2: ejecute siempre con -XX:+PrintComstacktion , -XX:+PrintComstacktion -verbose:gc , etc., para que pueda verificar que el comstackdor y otras partes de la JVM no están haciendo un trabajo inesperado durante su fase de temporización.

Regla 2.1: Imprima mensajes al comienzo y al final de las fases de temporización y calentamiento, para que pueda verificar que no haya salida de la Regla 2 durante la fase de temporización.

Regla 3: tenga en cuenta la diferencia entre -client y -server, y OSR y comstackciones regulares. El -XX:+PrintComstacktion informa las comstackciones de OSR con un signo en para indicar el punto de entrada no inicial, por ejemplo: Trouble$1::run @ 2 (41 bytes) . Prefiere servidor a cliente, y regular a OSR, si buscas el mejor rendimiento.

Regla 4: tenga en cuenta los efectos de inicialización. No imprima por primera vez durante la fase de temporización, ya que imprime cargas e inicializa las clases. No cargue nuevas clases fuera de la fase de calentamiento (o fase de informe final), a menos que esté probando la carga de clases específicamente (y en ese caso solo cargue las clases de prueba). La Regla 2 es tu primera línea de defensa contra tales efectos.

Regla 5: tenga en cuenta los efectos de desoptimización y recomstackción. No tome ninguna ruta de código por primera vez en la fase de temporización, porque el comstackdor puede juntar y volver a comstackr el código, en base a una suposición optimista anterior de que la ruta no iba a utilizarse en absoluto. La Regla 2 es tu primera línea de defensa contra tales efectos.

Regla 6: Use las herramientas apropiadas para leer la mente del comstackdor, y espere sorprenderse por el código que produce. Inspeccione el código usted mismo antes de formar teorías sobre qué hace que algo sea más rápido o más lento.

Regla 7: reduzca el ruido en sus medidas. Ejecute su punto de referencia en una máquina silenciosa y ejecútelo varias veces, descartando valores atípicos. Use -Xbatch para serializar el comstackdor con la aplicación, y considere configurar -XX:CICompilerCount=1 para evitar que el comstackdor se ejecute en paralelo consigo mismo. Haga su mejor Xmx para reducir la sobrecarga de GC, configure Xmx (lo suficientemente grande) igual a Xms y use UseEpsilonGC si está disponible.

Regla 8: utilice una biblioteca para su punto de referencia, ya que es probablemente más eficiente y ya se depuró con este único propósito. Tales como JMH , Caliper o Bill y Paul Excelentes UCSD Benchmarks for Java .

Sé que esta pregunta ha sido marcada como respondida, pero quería mencionar dos bibliotecas que nos permiten escribir micro benchmarks

Caliper de Google

Introducción a tutoriales

  1. http://codingjunkie.net/micro-benchmarking-with-caliper/
  2. http://vertexlabs.co.uk/blog/caliper

JMH de OpenJDK

Introducción a tutoriales

  1. Evitando errores de evaluación comparativa en la JVM
  2. http://nitschinger.at/Using-JMH-for-Java-Microbenchmarking
  3. http://java-performance.info/jmh/

Las cosas importantes para los puntos de referencia de Java son:

  • Primero, calentar el JIT ejecutando el código varias veces antes de cronometrarlo
  • Asegúrese de ejecutarlo el tiempo suficiente para poder medir los resultados en segundos o (mejor) decenas de segundos
  • Si bien no puede llamar a System.gc() entre iteraciones, es una buena idea ejecutarlo entre pruebas, de modo que cada prueba tenga un espacio de memoria “limpio” para trabajar. (Sí, gc() es más una pista que una garantía, pero es muy probable que realmente se acumule basura en mi experiencia).
  • Me gusta mostrar las iteraciones y el tiempo, y un puntaje de tiempo / iteración que se puede escalar de modo que el “mejor” algoritmo obtenga un puntaje de 1.0 y otros se califiquen de manera relativa. Esto significa que puede ejecutar todos los algoritmos durante un tiempo prolongado, variando tanto el número de iteraciones como el tiempo, pero obteniendo resultados comparables.

Solo estoy en el proceso de bloguear sobre el diseño de un marco de referencia en .NET. Tengo un par de publicaciones anteriores que pueden darte algunas ideas: no todo será apropiado, por supuesto, pero sí algo de eso.

jmh es una adición reciente a OpenJDK y ha sido escrito por algunos ingenieros de rendimiento de Oracle. Sin duda vale la pena echar un vistazo.

El jmh es un arnés de Java para comstackr, ejecutar y analizar pruebas de nano / micro / macro escritas en Java y otros lenguajes que apuntan a la JVM.

Datos muy interesantes enterrados en la muestra prueban los comentarios .

Ver también:

  • Evitando errores de evaluación comparativa en la JVM
  • Discusión sobre las principales fortalezas de jmh .

¿Debería el índice de referencia medir el tiempo / la iteración o las iteraciones / tiempo, y por qué?

Depende de lo que intentas probar. Si está interesado en la latencia, use tiempo / iteración y, si está interesado en el rendimiento, use iteraciones / tiempo.

Asegúrese de usar de alguna manera los resultados que se computan en el código de referencia. De lo contrario, su código puede optimizarse.

Si está tratando de comparar dos algoritmos, haga al menos dos puntos de referencia en cada uno, alternando el orden. es decir:

 for(i=1..n) alg1(); for(i=1..n) alg2(); for(i=1..n) alg2(); for(i=1..n) alg1(); 

He encontrado algunas diferencias notables (5-10% a veces) en el tiempo de ejecución del mismo algoritmo en diferentes pases.

Además, asegúrese de que n es muy grande, de modo que el tiempo de ejecución de cada ciclo sea como mínimo de 10 segundos aproximadamente. Cuantas más iteraciones, las cifras más significativas en su tiempo de referencia y más confiable que los datos.

Existen muchas trampas posibles para escribir micro-puntos de referencia en Java.

Primero: debe calcular con todo tipo de eventos que llevan más o menos tiempo al azar: recolección de basura, efectos de caché (de SO para archivos y de CPU para memoria), IO, etc.

Segundo: no puede confiar en la precisión de los tiempos medidos durante intervalos muy cortos.

Tercero: la JVM optimiza su código durante la ejecución. Por lo tanto, diferentes ejecuciones en la misma instancia de JVM serán cada vez más rápidas.

Mis recomendaciones: haga que su punto de referencia se ejecute unos segundos, que es más confiable que un tiempo de ejecución en milisegundos. Calentamiento de la JVM (significa ejecutar el punto de referencia al menos una vez sin medir, que la JVM puede ejecutar optimizaciones). Y ejecute su punto de referencia varias veces (tal vez 5 veces) y tome el valor mediano. Ejecute cada micro-punto de referencia en una nueva instancia de JVM (llamada para cada nueva prueba de Java nueva), de lo contrario, los efectos de optimización de la JVM pueden influir en las pruebas de ejecución posteriores. No ejecute cosas que no se ejecutan en la fase de calentamiento (ya que esto podría desencadenar la carga de clases y la recomstackción).

También se debe tener en cuenta que también podría ser importante analizar los resultados del micro benchmark cuando se comparan diferentes implementaciones. Por lo tanto , se debe hacer una prueba de significancia .

Esto se debe a que la implementación A podría ser más rápida durante la mayoría de las ejecuciones del índice de referencia que la implementación B Pero A también podría tener una mayor dispersión, por lo que el beneficio de rendimiento medido de A no tendrá ninguna importancia en comparación con B

Por lo tanto, también es importante escribir y ejecutar un micro benchmark correctamente, pero también analizarlo correctamente.

http://opt.sourceforge.net/ Java Micro Benchmark: tareas de control necesarias para determinar las características comparativas de rendimiento del sistema informático en diferentes plataformas. Puede usarse para guiar las decisiones de optimización y para comparar diferentes implementaciones de Java.

Para agregar otro excelente consejo, también me gustaría tener en cuenta lo siguiente:

Para algunas CPU (por ejemplo, el rango Intel Core i5 con TurboBoost), la temperatura (y la cantidad de núcleos que se utilizan actualmente, así como su porcentaje de utilización) afecta la velocidad del reloj. Dado que las CPU se sincronizan dinámicamente, esto puede afectar sus resultados. Por ejemplo, si tiene una aplicación de subproceso único, la velocidad de reloj máxima (con TurboBoost) es mayor que para una aplicación que utiliza todos los núcleos. Por lo tanto, esto puede interferir con las comparaciones de rendimiento único y de subprocesos múltiples en algunos sistemas. Tenga en cuenta que la temperatura y las fluctuaciones también afectan la duración de la frecuencia de Turbo.

Quizás un aspecto más importante sobre el que tienes control directo: ¡asegúrate de estar midiendo lo correcto! Por ejemplo, si usa System.nanoTime() para comparar un determinado bit de código, realice las llamadas a la tarea en lugares que tengan sentido para evitar medir cosas que no le interesan. Por ejemplo, no lo haga. hacer:

 long startTime = System.nanoTime(); //code here... System.out.println("Code took "+(System.nanoTime()-startTime)+"nano seconds"); 

El problema es que no está llegando inmediatamente a la hora de finalización cuando el código ha terminado. En cambio, intente lo siguiente:

 final long endTime, startTime = System.nanoTime(); //code here... endTime = System.nanoTime(); System.out.println("Code took "+(endTime-startTime)+"nano seconds");