Java 8: rendimiento de flujos frente a colecciones

Soy nuevo en Java 8. Todavía no conozco la API en profundidad, pero he hecho una pequeña referencia informal para comparar el rendimiento de la nueva API de Streams frente a las buenas colecciones anteriores.

La prueba consiste en filtrar una lista de Integer , y para cada número par, calcular la raíz cuadrada y almacenarla en una List de resultados de Double .

Aquí está el código:

  public static void main(String[] args) { //Calculating square root of even numbers from 1 to N int min = 1; int max = 1000000; List sourceList = new ArrayList(); for (int i = min; i < max; i++) { sourceList.add(i); } List result = new LinkedList(); //Collections approach long t0 = System.nanoTime(); long elapsed = 0; for (Integer i : sourceList) { if(i % 2 == 0){ result.add(Math.sqrt(i)); } } elapsed = System.nanoTime() - t0; System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9)); //Stream approach Stream stream = sourceList.stream(); t0 = System.nanoTime(); result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList()); elapsed = System.nanoTime() - t0; System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9)); //Parallel stream approach stream = sourceList.stream().parallel(); t0 = System.nanoTime(); result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList()); elapsed = System.nanoTime() - t0; System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9)); }. 

Y aquí están los resultados para una máquina de doble núcleo:

  Collections: Elapsed time: 94338247 ns (0,094338 seconds) Streams: Elapsed time: 201112924 ns (0,201113 seconds) Parallel streams: Elapsed time: 357243629 ns (0,357244 seconds) 

Para esta prueba en particular, las transmisiones son aproximadamente el doble de lentas que las recostackciones, y el paralelismo no ayuda (¿o lo estoy usando de la manera incorrecta?).

Preguntas:

  • ¿Es justa esta prueba? ¿He cometido algún error?
  • ¿Las transmisiones son más lentas que las colecciones? ¿Alguien ha hecho un buen punto de referencia formal sobre esto?
  • ¿Qué enfoque debería esforzarme?

Resultados actualizados

Ejecuté la prueba 1k veces después del calentamiento de JVM (iteraciones de 1k) según lo indicado por @pveentjer:

  Collections: Average time: 206884437,000000 ns (0,206884 seconds) Streams: Average time: 98366725,000000 ns (0,098367 seconds) Parallel streams: Average time: 167703705,000000 ns (0,167704 seconds) 

En este caso, las transmisiones son más efectivas. Me pregunto qué se observaría en una aplicación donde la función de filtrado solo se llama una o dos veces durante el tiempo de ejecución.

  1. Deje de utilizar LinkedList para cualquier cosa que no sea una eliminación importante desde el medio de la lista mediante iterator.

  2. Deje de escribir código de evaluación comparativa a mano, use JMH .

Puntos de referencia adecuados:

 @OutputTimeUnit(TimeUnit.NANOSECONDS) @BenchmarkMode(Mode.AverageTime) @OperationsPerInvocation(StreamVsVanilla.N) public class StreamVsVanilla { public static final int N = 10000; static List sourceList = new ArrayList<>(); static { for (int i = 0; i < N; i++) { sourceList.add(i); } } @Benchmark public List vanilla() { List result = new ArrayList<>(sourceList.size() / 2 + 1); for (Integer i : sourceList) { if (i % 2 == 0){ result.add(Math.sqrt(i)); } } return result; } @Benchmark public List stream() { return sourceList.stream() .filter(i -> i % 2 == 0) .map(Math::sqrt) .collect(Collectors.toCollection( () -> new ArrayList<>(sourceList.size() / 2 + 1))); } } 

Resultado:

 Benchmark Mode Samples Mean Mean error Units StreamVsVanilla.stream avgt 10 17.588 0.230 ns/op StreamVsVanilla.vanilla avgt 10 10.796 0.063 ns/op 

Del mismo modo que esperaba que la implementación de la transmisión fuese bastante más lenta. JIT puede alinear todas las cosas lambda pero no produce un código tan conciso como la versión de vanilla.

Generalmente, las secuencias de Java 8 no son una magia. No podían acelerar las cosas ya bien implementadas (con, probablemente, iteraciones simples o Java 5 para cada instrucción reemplazada por Iterable.forEach() y Collection.removeIf() ). Las transmisiones tratan más sobre la conveniencia y seguridad de la encoding. Comodidad: la compensación de velocidad funciona aquí.

1) Ve un tiempo de menos de 1 segundo usando su punto de referencia. Eso significa que puede haber una gran influencia de los efectos secundarios en sus resultados. Entonces, aumenté tu tarea 10 veces

  int max = 10000000; 

y ejecutó su punto de referencia. Mis resultados:

 Collections: Elapsed time: 8592999350 ns (8.592999 seconds) Streams: Elapsed time: 2068208058 ns (2.068208 seconds) Parallel streams: Elapsed time: 7186967071 ns (7.186967 seconds) 

sin editar ( int max = 1000000 ) se obtuvieron resultados

 Collections: Elapsed time: 113373057 ns (0.113373 seconds) Streams: Elapsed time: 135570440 ns (0.135570 seconds) Parallel streams: Elapsed time: 104091980 ns (0.104092 seconds) 

Es como sus resultados: la transmisión es más lenta que la recolección. Conclusión: se dedicó mucho tiempo a la transmisión / transmisión de valores de inicialización de flujo.

2) Después de boost la secuencia de tareas se hizo más rápido (eso está bien), pero la transmisión paralela se mantuvo demasiado lenta. ¿Qué pasa? Nota: usted tiene collect(Collectors.toList()) en su comando. Recolectar para una sola colección básicamente introduce un cuello de botella de rendimiento y una sobrecarga en caso de ejecución simultánea. Es posible estimar el costo relativo de los gastos generales reemplazando

 collecting to collection -> counting the element count 

Para transmisiones se puede hacer por collect(Collectors.counting()) . Obtuve resultados:

 Collections: Elapsed time: 41856183 ns (0.041856 seconds) Streams: Elapsed time: 546590322 ns (0.546590 seconds) Parallel streams: Elapsed time: 1540051478 ns (1.540051 seconds) 

¡Eso es para una gran tarea! ( int max = 10000000 ) Conclusión: la recolección de elementos para la recolección tomó la mayoría del tiempo. La parte más lenta es agregar a la lista. Por cierto, ArrayList simple se usa para Collectors.toList() .

  public static void main(String[] args) { //Calculating square root of even numbers from 1 to N int min = 1; int max = 10000000; List sourceList = new ArrayList<>(); for (int i = min; i < max; i++) { sourceList.add(i); } List result = new LinkedList<>(); //Collections approach long t0 = System.nanoTime(); long elapsed = 0; for (Integer i : sourceList) { if(i % 2 == 0){ result.add( doSomeCalculate(i)); } } elapsed = System.nanoTime() - t0; System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9)); //Stream approach Stream stream = sourceList.stream(); t0 = System.nanoTime(); result = stream.filter(i -> i%2 == 0).map(i -> doSomeCalculate(i)) .collect(Collectors.toList()); elapsed = System.nanoTime() - t0; System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9)); //Parallel stream approach stream = sourceList.stream().parallel(); t0 = System.nanoTime(); result = stream.filter(i -> i%2 == 0).map(i -> doSomeCalculate(i)) .collect(Collectors.toList()); elapsed = System.nanoTime() - t0; System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9)); } static double doSomeCalculate(int input) { for(int i=0; i<100000; i++){ Math.sqrt(i+input); } return Math.sqrt(input); } 

Cambié un poco el código, ejecuté en mi mac pro libro que tiene 8 núcleos, obtuve un resultado razonable:

Colecciones: Tiempo transcurrido: 1522036826 ns (1.522037 segundos)

Flujos: tiempo transcurrido: 4315833719 ns (4.315834 segundos)

Flujos en paralelo: tiempo transcurrido: 261152901 ns (0.261153 segundos)

Por lo que estás tratando de hacer, no usaría las API de Java de todos modos. Hay un montón de boxeo / unboxing pasando, por lo que hay una gran sobrecarga de rendimiento.

Personalmente, creo que una gran cantidad de API diseñados son una porquería porque crean una gran cantidad de desechos de objetos.

Intenta utilizar matrices primitivas de double / int y trata de hacerlo de una sola hebra y ver cuál es el rendimiento.

PD: Es posible que desee echarle un vistazo a JMH para que se encargue de hacer el punto de referencia. Cuida algunos de los problemas típicos, como calentar la JVM.