Benchmarking Java 8: ¿Qué es más rápido?

0
4567

En este tutorial realizaremos un benchmarking de todos los ejemplos de la primera parte, descubriendo las ventajas y desventajas que las Lambdas y Streams nos proporcionan.

Índice de contenidos

1. Introducción

Esta es la segunda parte del tutorial sobre Java 8 que he realizado. En esta segunda parte me centraré en comprobar cuales son las ventajas y desventajas de usar Streams y Lambdas, sacando en conclusión cuándo y cómo usar cada una de las herramientas que nos aportan. Para este tutorial he contado con David Gómez para ayudarme a realizar benchmarking lo más fiable posible.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 17′ (3 Ghz Intel Core 2 Duo, 8GB DDR3).
  • Sistema Operativo: Mac OS El Capitán 10.11
  • Entorno de desarrollo: IntelliJ Idea 2016.2
  • JavaSE build 1.8.0_77-b03
  • Apache Maven 3.1.1

3. Microbenchmarking a mano

Las modificaciones que van a aparecer respecto al ejemplo anterior son las siguientes:

1ª setUp() y testWith()

El método testWith nos permitirá pasar por parámetro el método en forma de Lambda (Proveedor) que queremos realizar para aplicar una serie de acciones antes y después de ejecutar el test, entre ellas la línea System.gc();. Esta se utiliza para ejecutar el Garbage Collector antes del test para evitar que cualquier GC se ejecute en medio de un test y pueda modificar nuestro benchmark.

Para monitorizar el GC y asegurarnos de que no se ejecuta entre las marcas Start – End editamos la Run/Debug configuration y en VM options debemos usar el parámetro -XX:+PrintGC. Además, configuraremos el tamaño inicial del heap para que tenga 4GB con el parámetro -Xms4092m. Para saber más acerca del GC, podéis ver este tutorial de adictos que habla de ello.

Captura de pantalla 2016-05-24 a las 15.46.09

El setUp creará el mismo carrito antes de cada test y así probar la misma carga en todos ellos.

2ª Contador

Este contador nos permitirá saber el número de iteraciones que hacen cada uno de los diferentes tipos de recorrer un stream

Lo siguiente que debemos hacer, es crear unos nuevos test para cada método. El código es el siguiente:

Nuevos test

3.1. Resultados individuales

Una vez creados, empezamos el primer benchmark. A continuación dejaré varios logs con los resultados que he recibido, empezando por el LOAD_LEVEL que había en cada log.

Nota: LOAD_LEVEL se llama en el código LOAD_LEVELX2 ya que el valor negativo lo incluyo justo en la mitad, por lo que añado un LOAD_LEVEL al principio y otro al final.

Captura de pantalla 2016-06-01 a las 9.59.51

Como podéis observar, entre las marcas Start – End no se ejecuta ningún Garbage Collector, evitando así falsear los tiempos de ejecución.

A continuación realizaré las pruebas con más LOAD_LEVEL con un ordenador más potente para ver los resultados. El hardware utilizado para los siguientes benchmarks es:

  • Macbook Pro 15′ Intel core i5 2’4GHz, 8GB DDR3

3.2. Resultados por iteración

Las cinco iteraciones mostradas en los gráficos son con LOAD_LEVEL= 2000L y LOAD_LEVEL= 20_000_000L

LOAD_LEVEL= 2000L

Captura de pantalla 2016-06-01 a las 11

LOAD_LEVEL= 20_000_000L

Captura de pantalla 2016-06-01 a las 11

3.3. Media y Desviación típica

La media está realizada con la siguiente fórmula en cada LOAD_LEVEL:

Captura de pantalla 2016-06-01 a las 10.57.36

Captura de pantalla 2016-06-01 a las 11.25.35

Captura de pantalla 2016-06-01 a las 11.26.20

3.4. Curva de respuesta

Captura de pantalla 2016-06-01 a las 10.18.35

Gracias a los gráficos se puede comprobar que hay dos métodos cuya curva de respuesta y resultados son mucho mejores que los demás: anyMatchParallel y findAnyParallel. Si bien se puede observar que en valores bajos es incluso más pesado el trabajo que realizan las lambdas y streams que un bucle for.

Hay un resultado que me gustaría remarcar también y es el de findAny, cuya Desviación típica es la más grande, lo que viene a traducirse que sus resultados son menos uniformes y por lo tanto más inestables.

4. ¿Por qué?

Una vez obtenidos los resultados, vamos a averiguar el por qué de estos contando el número de iteraciones que realizan. Para ello modificamos los métodos y test para que usen el counter que ya teníamos creado.

¡OJO! El código que a continuación pongo solo debe usarse para el contador, no para el benchmark. Esto podría falsear mucho los resultados.

Añadimos el método peek() el cual recibe un consumidor. El método peek realiza un método pasado como una lambda pero no afecta a ninguna otra parte del stream. Es decir, es una operación intermedia cuya acción es invisible al resto de métodos. En este caso lo usamos para lanzar el método incrementAndGet() de AtomicLong, con el cual evitaremos una condición de carrera al contar iteraciones con multihilo

El resultado:

Solamente el ejemplo de Programación imperativa recorre todos los datos. Para dicho ejemplo necesitaríamos poner una comprobación más con un break y dicha práctica no es muy recomendable. Los métodos anyMatch, findAny, findFirst y findFirstParallel recorren hasta la primera coincidencia. anyMatchParallel y findAnyParallel recorren 1/4 del total ya que divide la carga por hilos, en mi caso 2 hilos. Cuando uno de esos hilos encuentra alguna (any) coincidencia se paran los demás.

Tanto findAnyParallel como anyMatchParallel hay veces que muestran los siguientes resultados:

Como ya dije, estos métodos se dividen por hilos y dependiendo de en qué momento el hilo esté más o menos ocupado, así se repartirán la carga recogiendo unos hilos más carga que otros.

A continuación dejo los resultados que he obtenido en otro ordenador

  • Macbook Pro 15′ Intel core i5 2’4GHz, 8GB DDR3

5. Conclusiones

Una vez hechas todas las pruebas incluso en difrentes hardwares queda claro que, si se trata de grandes volúmenes de información, anyMatchParallel y findAnyParallel suponen una ventaja absoluta en cuanto a claridad y sencillez del código y rapidez de ejecución. Hemos visto que incluso en volúmenes muy pequeños sería hasta contraproducente usar streams y lambdas. Cuándo usarlos depende de muchos factores que cada desarrollador debe valorar por sí mismo.

6. Enlaces

Repositorio de github

Documento pdf con los resultados

Dejar respuesta

Please enter your comment!
Please enter your name here