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

870

¿Cómo se escribe (y ejecuta) un micro punto de referencia correcto en Java?

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

Ejemplo: ¿Debe el punto de referencia medir tiempo / iteración o iteraciones / tiempo, y por qué?

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

John Nilsson
fuente
Consulte [esta pregunta] [1] de hace unos minutos para obtener información relacionada. editar: lo siento, se supone que esto no es una respuesta. Debería haber publicado como comentario. [1]: stackoverflow.com/questions/503877/…
Tiago
Fue después de planear referir el cartel de esa pregunta a una pregunta como esta que noté que esta pregunta no existía. Así que aquí está, con suerte reunirá algunos buenos consejos con el tiempo.
John Nilsson
55
Java 9 podría proporcionar algunas características para micro-benchmarking: openjdk.java.net/jeps/230
Raedwald
1
@Raedwald Creo que que tiene como objetivo PEC para añadir algún punto de referencia micro al código JDK, pero no creo que JMH será incluido en el JDK ...
assylias
1
@Raedwald Hola desde el futuro. No hizo el corte .
Michael

Respuestas:

787

Consejos sobre cómo escribir micro benchmarks de los creadores de Java HotSpot :

Regla 0: Lea un documento de buena reputación 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 de JVM.

Regla 1: Incluya siempre una fase de calentamiento que ejecute su núcleo de prueba hasta el final, lo suficiente como para activar todas las inicializaciones y compilaciones antes de la (s) fase (s) de temporización. (Menos iteraciones están bien en la fase de calentamiento. La regla general es varias decenas de miles de iteraciones de bucle interno).

Regla 2: Siempre ejecuta con -XX:+PrintCompilation, -verbose:gc, etc., para que pueda verificar que el compilador 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 principio 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 -clienty -server, y OSR y compilaciones regulares. La -XX:+PrintCompilationbandera informa compilaciones OSR con un signo en para denotar 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 su fase de temporización, ya que la impresión carga 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 clase específicamente (y en ese caso cargue solo las clases de prueba). La regla 2 es su primera línea de defensa contra tales efectos.

Regla 5: Tenga en cuenta los efectos de desoptimización y recompilación. No tome ninguna ruta de código por primera vez en la fase de temporización, porque el compilador puede desechar y volver a compilar el código, en base a una suposición optimista anterior de que la ruta no se iba a utilizar en absoluto. La regla 2 es su primera línea de defensa contra tales efectos.

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

Regla 7: Reduce el ruido en tus medidas. Ejecute su punto de referencia en una máquina silenciosa, y ejecútelo varias veces, descartando valores atípicos. Utilícelo -Xbatchpara serializar el compilador con la aplicación y considere la configuración -XX:CICompilerCount=1para evitar que el compilador se ejecute en paralelo consigo mismo. Haga todo lo posible para reducir la sobrecarga del GC, establezca Xmx(lo suficientemente grande) iguales Xmsy utilícelo UseEpsilonGCsi está disponible.

Regla 8: Use una biblioteca para su punto de referencia, ya que probablemente sea más eficiente y ya se haya depurado para este único propósito. Como JMH , Caliper o Bill and Paul's Excellent UCSD Benchmarks para Java .

Eugene Kuleshov
fuente
55
Este también fue un artículo interesante: ibm.com/developerworks/java/library/j-jtp12214
John Nilsson
143
Además, nunca use System.currentTimeMillis () a menos que esté de acuerdo con una precisión de + o - 15 ms, lo cual es típico en la mayoría de las combinaciones OS + JVM. Utilice System.nanoTime () en su lugar.
Scott Carey
55
Algunos documentos de javaOne: azulsystems.com/events/javaone_2009/session/…
bestsss
94
Cabe señalar que System.nanoTime()no se garantiza que sea más preciso que System.currentTimeMillis(). Solo se garantiza que sea al menos tan preciso. Sin embargo, generalmente es mucho más preciso.
Gravity
41
La razón principal por la que se debe usar en System.nanoTime()lugar de System.currentTimeMillis()es que se garantiza que la primera aumentará monotónicamente. Restar los valores devueltos dos currentTimeMillisinvocaciones en realidad puede dar resultados negativos, posiblemente porque el tiempo del sistema fue ajustado por algún demonio NTP.
Waldheinz
239

Sé que esta pregunta se ha marcado como respondida, pero quería mencionar dos bibliotecas que nos ayudan a escribir micro puntos de referencia.

Pinza de Google

Tutoriales de iniciación

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

JMH de OpenJDK

Tutoriales de iniciación

  1. Evitar dificultades de evaluación comparativa en la JVM
  2. http://nitschinger.at/Using-JMH-for-Java-Microbenchmarking
  3. http://java-performance.info/jmh/
Aravind Yarram
fuente
37
+1 podría haberse agregado como la Regla 8 de la respuesta aceptada: Regla 8: debido a que muchas cosas pueden salir mal, ¡probablemente deberías usar una biblioteca existente en lugar de intentar hacerlo tú mismo!
assylias
8
@Pangea jmh es probablemente superior a Caliper hoy en día, ver también: groups.google.com/forum/#!msg/mechanical-sympathy/m4opvy4xq3U/…
assylias
87

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

  • Primero calienta el JIT ejecutando el código varias veces antes de cronometrar que
  • Asegúrese de ejecutarlo durante el tiempo suficiente para poder medir los resultados en segundos o (mejor) decenas de segundos
  • Si bien no puede llamar System.gc()entre iteraciones, es una buena idea ejecutarlo entre pruebas, de modo que con suerte cada prueba obtenga un espacio de memoria "limpio" para trabajar. (Sí, gc()es más una pista que una garantía, pero es muy probable que en mi experiencia realmente se acumule basura).
  • Me gusta mostrar las iteraciones y el tiempo, y una puntuación de tiempo / iteración que se puede escalar de modo que el "mejor" algoritmo obtenga una puntuación 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 aún obteniendo resultados comparables.

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 algunas pueden serlo.

Jon Skeet
fuente
3
Nitpick menor: IMO "para que cada prueba obtenga" debe ser "para que cada prueba pueda obtener" ya que la primera da la impresión de que las llamadas gc siempre liberan memoria no utilizada.
Sanjay T. Sharma
@ SanjayT.Sharma: Bueno, la intención es que realmente lo haga. Si bien no está estrictamente garantizado, en realidad es una pista bastante fuerte. Se editará para ser más claro.
Jon Skeet
1
No estoy de acuerdo con llamar a System.gc (). Es una pista, eso es todo. Ni siquiera "con suerte hará algo". Nunca deberías llamarlo. Esto es programación, no arte.
gyorgyabraham
13
@ gyabraham: Sí, es una pista, pero es algo que he observado que generalmente se toma. Entonces, si no le gusta usar System.gc(), ¿cómo propone minimizar la recolección de basura en una prueba debido a los objetos creados en pruebas anteriores? Soy pragmático, no dogmático.
Jon Skeet
99
@ gyabraham: No sé a qué te refieres con "gran respaldo". ¿Puede dar más detalles y, una vez más, tiene una propuesta para dar mejores resultados? Dije explícitamente que no es una garantía ...
Jon Skeet
48

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

El jmh es un arnés de Java para construir, ejecutar y analizar puntos de referencia nano / micro / macro escritos en Java y otros lenguajes destinados a la JVM.

Piezas muy interesantes de la información enterrados en las pruebas de muestra comentarios .

Ver también:

asilias
fuente
1
Consulte también esta publicación de blog: psy-lob-saw.blogspot.com/2013/04/… para obtener detalles sobre cómo comenzar con JMH.
Nitsan Wakart
FYI, JEP 230: Microbenchmark Suite es una propuesta de OpenJDK basada en este proyecto de Java Microbenchmark Harness (JMH) . No realizó el corte para Java 9, pero puede agregarse más tarde.
Basil Bourque
23

¿Debe el punto de referencia medir tiempo / iteración o iteraciones / tiempo, y por qué?

Depende de lo que intente probar.

Si está interesado en la latencia , use tiempo / iteración y si está interesado en el rendimiento , use iteraciones / tiempo.

Peter Lawrey
fuente
16

Si está tratando de comparar dos algoritmos, haga al menos dos puntos de referencia para 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 sea ​​muy grande, de modo que el tiempo de ejecución de cada ciclo sea de al menos 10 segundos más o menos. Cuantas más iteraciones, cifras más significativas en su tiempo de referencia y más confiables son los datos.

Dormir
fuente
55
Naturalmente, cambiar el orden influye en el tiempo de ejecución. Las optimizaciones JVM y los efectos de almacenamiento en caché van a funcionar aquí. Lo mejor es 'calentar' la optimización JVM, realizar múltiples ejecuciones y comparar cada prueba en una JVM diferente.
Mnementh
15

Asegúrese de utilizar de alguna manera los resultados que se calculan en código de referencia. De lo contrario, su código se puede optimizar.

Peter Štibraný
fuente
13

Hay muchas posibles dificultades para escribir micro-puntos de referencia en Java.

Primero: debe calcular con todo tipo de eventos que toman tiempo más o menos al azar: recolección de basura, efectos de almacenamiento en caché (del sistema operativo para archivos y de la 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 mientras se ejecuta. 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, eso es más confiable que un tiempo de ejecución durante milisegundos. Calentar la JVM (significa ejecutar el punto de referencia al menos una vez sin medir, para que la JVM pueda ejecutar optimizaciones). Y ejecute su punto de referencia varias veces (tal vez 5 veces) y tome el valor medio. Ejecute cada micro-punto de referencia en una nueva instancia de JVM (llame a cada nuevo punto de referencia Java nuevo) de lo contrario, los efectos de optimización de la JVM pueden influir en las pruebas posteriores. No ejecute cosas que no se ejecutan en la fase de calentamiento (ya que esto podría desencadenar carga de clase y recompilación).

Mnementh
fuente
8

También se debe tener en cuenta que también podría ser importante analizar los resultados del micro punto de referencia al comparar diferentes implementaciones. Por lo tanto, una prueba de significación debe hacer .

Esto se debe a que la implementación Apuede ser más rápida durante la mayoría de las ejecuciones del benchmark que la implementación B. Pero Atambién podría tener una mayor difusión, por lo que el beneficio de rendimiento medido Ano será de ninguna importancia en comparación conB .

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

SpaceTrucker
fuente
8

Para agregar al otro excelente consejo, también sería consciente de lo siguiente:

Para algunas CPU (por ejemplo, la gama Intel Core i5 con TurboBoost), la temperatura (y el número de núcleos que se utilizan actualmente, así como su porcentaje de utilización) afecta la velocidad del reloj. Dado que las CPU están sincronizadas 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 del rendimiento de subprocesos simples y múltiples en algunos sistemas. Tenga en cuenta que la temperatura y los voltajes también afectan el tiempo que se mantiene la frecuencia Turbo.

Quizás un aspecto más fundamental sobre el que tiene control directo: ¡asegúrese de medir lo correcto! Por ejemplo, si está utilizando System.nanoTime()para comparar un determinado código, coloque las llamadas a la tarea en lugares que tengan sentido para evitar medir cosas que no le interesan. Por ejemplo, no haga:

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

El problema es que no está obteniendo de inmediato la hora de finalización cuando el código ha terminado. En su lugar, intente lo siguiente:

final long endTime, startTime = System.nanoTime();
//code here...
endTime = System.nanoTime();
System.out.println("Code took "+(endTime-startTime)+"nano seconds");
Sina Madani
fuente
Sí, es importante no hacer un trabajo no relacionado dentro de la región cronometrada, pero su primer ejemplo todavía está bien. Solo hay una llamada a println, no una línea de encabezado separada o algo así, y System.nanoTime()debe evaluarse como el primer paso para construir la cadena arg para esa llamada. No hay nada que un compilador pueda hacer con el primero que no pueda hacer con el segundo, y ninguno de los dos lo alienta a hacer un trabajo adicional antes de grabar un tiempo de parada.
Peter Cordes
7

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. Se puede utilizar para guiar las decisiones de optimización y comparar diferentes implementaciones de Java.

Yuriy
fuente
2
Parece solo comparar el hardware JVM +, no una pieza arbitraria de código Java.
Stefan L