¿Cómo escribo un micro-benchmark correcto en Java?
jvm benchmarking (11)
¿Cómo escribes (y ejecutas) un micro-benchmark correcto en Java?
Estoy buscando aquí ejemplos de código y comentarios que ilustran varias cosas para pensar.
Ejemplo: ¿El punto de referencia debe medir el tiempo / iteración o iteraciones / tiempo, y por qué?
Relacionados: ¿Es aceptable el cronómetro de benchmarking?
¿Debería el punto de referencia medir el tiempo / iteración o iteraciones / tiempo, y por qué?
Depende de lo que estés intentando probar. Si está interesado en la latencia, use el tiempo / iteración y si está interesado en el rendimiento, use iteraciones / tiempo.
Asegúrese de que de alguna manera utiliza los resultados que se calculan en código de referencia. De lo contrario su código puede ser optimizado de distancia.
Consejos sobre cómo escribir micro benchmarks de los creadores de Java HotSpot :
Regla 0: lea un artículo 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 de JVM.
Regla 1: incluya siempre una fase de calentamiento que ejecute su kernel de prueba hasta el final, lo suficiente como para activar todas las inicializaciones y compilaciones antes de sincronizar las fases. (Menos repeticiones están 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:+PrintCompilation
, -XX:+PrintCompilation
-verbose:gc
, etc., para que pueda verificar que el compilador y otras partes de la JVM no estén realizando un trabajo inesperado durante su fase de sincronización.
Regla 2.1: imprima los mensajes al principio y al final de las fases de tiempo y calentamiento, para que pueda verificar que no haya salida de la Regla 2 durante la fase de tiempo.
Regla 3: ser consciente de la diferencia entre -client y -server, y OSR y compilaciones regulares. El -XX:+PrintCompilation
informa las compilaciones de OSR con un signo en para indicar el punto de entrada no inicial, por ejemplo: Trouble$1::run @ 2 (41 bytes)
. Prefiere el servidor al cliente, y normal a OSR, si está buscando el mejor rendimiento.
Regla 4: ser consciente de los efectos de inicialización. No imprima por primera vez durante la fase de sincronización, ya que la impresión carga e inicializa las clases. No cargue clases nuevas fuera de la fase de calentamiento (o la fase de informe final), a menos que esté probando la carga de clases 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: ser consciente de los efectos de la desoptimización y la recompilación. No tome ninguna ruta de código por primera vez en la fase de sincronización, ya que el compilador puede eliminar y volver a compilar el código, basándose en 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. Inspecciona el código por ti mismo antes de formar teorías sobre qué 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. Use -Xbatch
para serializar el compilador con la aplicación, y considere configurar -XX:CICompilerCount=1
para evitar que el compilador se ejecute en paralelo consigo mismo. Haga su mejor Xmx
para reducir la sobrecarga del GC, establezca Xmx
(lo suficientemente grande) como Xms
y use UseEpsilonGC
si está disponible.
Regla 8: use una biblioteca para su índice de referencia, ya que probablemente sea más eficiente y ya se haya depurado con este único propósito. Como JMH , Caliper o Bill y los Excelentes puntos de referencia de UCSD de Paul para Java .
Hay muchos escollos posibles para escribir micro-puntos de referencia en Java.
Primero: tiene que 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 en intervalos muy cortos.
Tercero: la JVM optimiza tu 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 índice de referencia se ejecute unos segundos, eso es más confiable que un tiempo de ejecución en 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-benchmark en una nueva instancia de JVM (llame para cada nuevo Java de referencia), de lo contrario, los efectos de optimización de la JVM pueden influir en las últimas pruebas en ejecución. No ejecute cosas, que no se ejecutan en la fase de calentamiento (ya que esto podría desencadenar la carga y recompilación de clases).
Las cosas importantes para los puntos de referencia de Java son:
- Calienta el JIT primero ejecutando el código varias veces antes de cronometrarlo
- 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 a
System.gc()
entre iteraciones, es una buena idea ejecutarlo entre pruebas, de modo que se espera que cada prueba obtenga un espacio de memoria "limpio" para trabajar. (Sí,gc()
es más una sugerencia que una garantía, pero es muy probable que realmente se recoja basura en mi experiencia). - Me gusta mostrar las iteraciones y el tiempo, y una puntuación de tiempo / iteración que se puede escalar de manera 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 el número de iteraciones y el tiempo, pero obteniendo resultados comparables.
Estoy en el proceso de bloguear sobre el diseño de un marco de referencia en .NET. Tengo un couple de publicaciones anteriores que pueden darte algunas ideas; no todo será apropiado, por supuesto, pero algunas pueden serlo.
Para agregar a los otros excelentes consejos, también debería tener en cuenta lo siguiente:
Para algunas CPU (por ejemplo, el rango de 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 están sincronizadas dinámicamente, esto puede afectar sus resultados. Por ejemplo, si tiene una aplicación de un solo hilo, la velocidad máxima del reloj (con TurboBoost) es mayor que para una aplicación que usa todos los núcleos. Por lo tanto, esto puede interferir con las comparaciones de rendimiento de subprocesos múltiples y múltiples en algunos sistemas. Tenga en cuenta que la temperatura y los voltajes también afectan la duración de la frecuencia de Turbo.
Tal vez sea un aspecto fundamentalmente importante sobre el que tienes control directo: ¡asegúrate de medir lo correcto! Por ejemplo, si está usando System.nanoTime()
para System.nanoTime()
un bit de código en particular, ponga las llamadas a la asignación en lugares que tengan sentido para evitar medir cosas en las que no está interesado. Por ejemplo, no hacer:
long startTime = System.nanoTime();
//code here...
System.out.println("Code took "+(System.nanoTime()-startTime)+"nano seconds");
El problema es que no está obteniendo inmediatamente 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");
Sé que esta pregunta ha sido marcada como contestada pero quería mencionar dos bibliotecas que nos permiten escribir micro puntos de referencia
Cómo empezar tutoriales
Cómo empezar tutoriales
Si está intentando comparar dos algoritmos, realice 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 pasadas ...
Además, asegúrese de que n sea muy grande, de modo que el tiempo de ejecución de cada bucle sea de al menos 10 segundos aproximadamente. Cuantas más iteraciones, más significativas sean las cifras en su tiempo de referencia y más confiables serán esos datos.
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 significación .
Esto se debe a que la implementación A
podría ser más rápida durante la mayoría de las ejecuciones del punto de referencia que la implementación B
Pero A
también podría tener una distribución más alta, por lo que el beneficio medido del rendimiento 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.
JMH es una adición reciente a OpenJDK y ha sido escrito por algunos ingenieros de rendimiento de Oracle. Ciertamente 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 dirigidos a la JVM.
Muy interesantes piezas de información enterradas en los comentarios de las pruebas de muestra .
Ver también:
http://opt.sourceforge.net/ Java Micro Benchmark: controle las tareas necesarias para determinar las características de rendimiento comparativo del sistema informático en diferentes plataformas. Puede utilizarse para guiar las decisiones de optimización y para comparar diferentes implementaciones de Java.