java - paquete - manual de programacion android pdf
Las secuencias de Java 8: ¿por qué la transmisión en paralelo es más lenta? (3)
Al hacer benchmarks, debe prestar atención al compilador JIT, y que los comportamientos de temporización pueden cambiar, cuando se activa el JIT. Si agrego una fase de calentamiento a su programa de prueba, la versión paralela es un poco más rápida que la versión secuencial . Aquí están los resultados:
Warmup...
Benchmark...
Run 0: sequential 0.12s - parallel 0.11s
Run 1: sequential 0.13s - parallel 0.08s
Run 2: sequential 0.15s - parallel 0.08s
Run 3: sequential 0.12s - parallel 0.11s
Run 4: sequential 0.13s - parallel 0.08s
A continuación está el código fuente completo, que he usado para esta prueba.
public static void main(String... args) {
String[] array = new String[1000000];
Arrays.fill(array, "AbabagalamagA");
System.out.println("Warmup...");
for (int i = 0; i < 100; ++i) {
sequential(array);
parallel(array);
}
System.out.println("Benchmark...");
for (int i = 0; i < 5; ++i) {
System.out.printf("Run %d: sequential %s - parallel %s/n",
i,
test(() -> sequential(array)),
test(() -> parallel(array)));
}
}
private static void sequential(String[] array) {
Arrays.stream(array).map(String::toLowerCase).collect(Collectors.toList());
}
private static void parallel(String[] array) {
Arrays.stream(array).parallel().map(String::toLowerCase).collect(Collectors.toList());
}
private static String test(Runnable runnable) {
long start = System.currentTimeMillis();
runnable.run();
long elapsed = System.currentTimeMillis() - start;
return String.format("%4.2fs", elapsed / 1000.0);
}
Estoy jugando con las transmisiones de Java 8 y no puedo entender los resultados de rendimiento que obtengo.
Tengo 2 núcleos de CPU (Intel i73520M), Windows 8 x64 y 64 bits de Java 8 actualización 5. Estoy haciendo un mapa simple sobre flujo / secuencia paralela de cadenas y encontré que la versión paralela es algo más lenta.
Cuando ejecuto este código:
String[] array = new String[1000000];
Arrays.fill(array, "AbabagalamagA");
Stream<String> stream = Arrays.stream(array);
long time1 = System.nanoTime();
List<String> list = stream.map((x) -> x.toLowerCase()).collect(Collectors.toList());
long time2 = System.nanoTime();
System.out.println((time2 - time1) / 1000000f);
... Obtengo un resultado de alrededor de 600. Esta versión, que usa transmisión paralela:
String[] array = new String[1000000];
Arrays.fill(array, "AbabagalamagA");
Stream<String> stream = Arrays.stream(array).parallel();
long time1 = System.nanoTime();
List<String> list = stream.map((x) -> x.toLowerCase()).collect(Collectors.toList());
long time2 = System.nanoTime();
System.out.println((time2 - time1) / 1000000f);
... me da algo sobre 900.
¿No debería la versión paralela ser más rápida, considerando el hecho de que tengo 2 núcleos de CPU? ¿Podría alguien darme una pista de por qué la versión paralela es más lenta?
El uso de varios subprocesos para procesar sus datos tiene algunos costos iniciales de configuración, por ejemplo, la inicialización del grupo de subprocesos. Estos costos pueden superar la ganancia de usar esos hilos, especialmente si el tiempo de ejecución ya es bastante bajo. Además, si hay contención, por ejemplo, otros subprocesos en ejecución, procesos en segundo plano, etc., el rendimiento del procesamiento en paralelo puede disminuir aún más.
Este problema no es nuevo para el procesamiento paralelo. Este artículo proporciona algunos detalles a la luz de Java 8 parallel()
y algunas cosas más a considerar: http://java.dzone.com/articles/think-twice-using-java-8
Hay varios problemas sucediendo aquí en paralelo, por así decirlo.
El primero es que resolver un problema en paralelo siempre implica realizar más trabajo real que hacerlo secuencialmente. Overhead está involucrado en dividir el trabajo entre varios hilos y unir o fusionar los resultados. Los problemas como la conversión de cadenas cortas a minúsculas son lo suficientemente pequeñas como para correr el riesgo de quedar inundadas por la división paralela.
El segundo problema es que el programa de evaluación comparativa Java es muy sutil, y es muy fácil obtener resultados confusos. Dos problemas comunes son la compilación de JIT y la eliminación del código muerto. Los benchmarks cortos a menudo terminan antes o durante la compilación de JIT, por lo que no están midiendo el rendimiento máximo, y de hecho podrían estar midiendo el JIT mismo. Cuando se produce la compilación es algo no determinista, por lo que puede causar que los resultados varíen en gran medida también.
Para puntos de referencia sintéticos pequeños, la carga de trabajo a menudo calcula los resultados que se descartan. Los compiladores JIT son bastante buenos para detectar esto y eliminar el código que no produce resultados que se usan en ningún lado. Es probable que esto no esté sucediendo en este caso, pero si se juega con otras cargas de trabajo sintéticas, sin duda puede suceder. Por supuesto, si el JIT elimina la carga de trabajo de referencia, hace que el punto de referencia sea inútil.
Recomiendo encarecidamente utilizar un marco de evaluación comparativa bien desarrollado, como JMH lugar de utilizar uno propio. JMH tiene instalaciones para ayudar a evitar las trampas comunes de evaluación comparativa, incluidas estas, y es bastante fácil de configurar y ejecutar. Aquí está su punto de referencia convertido para usar JMH:
package com..questions;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.*;
public class SO23170832 {
@State(Scope.Benchmark)
public static class BenchmarkState {
static String[] array;
static {
array = new String[1000000];
Arrays.fill(array, "AbabagalamagA");
}
}
@GenerateMicroBenchmark
@OutputTimeUnit(TimeUnit.SECONDS)
public List<String> sequential(BenchmarkState state) {
return
Arrays.stream(state.array)
.map(x -> x.toLowerCase())
.collect(Collectors.toList());
}
@GenerateMicroBenchmark
@OutputTimeUnit(TimeUnit.SECONDS)
public List<String> parallel(BenchmarkState state) {
return
Arrays.stream(state.array)
.parallel()
.map(x -> x.toLowerCase())
.collect(Collectors.toList());
}
}
Ejecuté esto usando el comando:
java -jar dist/microbenchmarks.jar ".*SO23170832.*" -wi 5 -i 5 -f 1
(Las opciones indican cinco iteraciones de calentamiento, cinco iteraciones de referencia y una JVM bifurcada.) Durante su ejecución, JMH emite muchos mensajes detallados, que he elidido. Los resultados del resumen son los siguientes.
Benchmark Mode Samples Mean Mean error Units
c.s.q.SO23170832.parallel thrpt 5 4.600 5.995 ops/s
c.s.q.SO23170832.sequential thrpt 5 1.500 1.727 ops/s
Tenga en cuenta que los resultados están en operaciones por segundo, por lo que parece que la ejecución paralela fue aproximadamente tres veces más rápida que la ejecución secuencial. Pero mi máquina solo tiene dos núcleos. Hmmm. ¡Y el error promedio por ejecución es en realidad más grande que el tiempo medio de ejecución! ¿WAT? Algo sospechoso está sucediendo aquí.
Esto nos lleva a un tercer problema. Mirando más de cerca la carga de trabajo, podemos ver que asigna un nuevo objeto String para cada entrada, y también recoge los resultados en una lista, lo que implica mucha reasignación y copia. Supongo que esto dará como resultado una buena cantidad de recolección de basura. Podemos ver esto volviendo a ejecutar el punto de referencia con los mensajes del GC habilitados:
java -verbose:gc -jar dist/microbenchmarks.jar ".*SO23170832.*" -wi 5 -i 5 -f 1
Esto da resultados como:
[GC (Allocation Failure) 512K->432K(130560K), 0.0024130 secs]
[GC (Allocation Failure) 944K->520K(131072K), 0.0015740 secs]
[GC (Allocation Failure) 1544K->777K(131072K), 0.0032490 secs]
[GC (Allocation Failure) 1801K->1027K(132096K), 0.0023940 secs]
# Run progress: 0.00% complete, ETA 00:00:20
# VM invoker: /Users/src/jdk/jdk8-b132.jdk/Contents/Home/jre/bin/java
# VM options: -verbose:gc
# Fork: 1 of 1
[GC (Allocation Failure) 512K->424K(130560K), 0.0015460 secs]
[GC (Allocation Failure) 933K->552K(131072K), 0.0014050 secs]
[GC (Allocation Failure) 1576K->850K(131072K), 0.0023050 secs]
[GC (Allocation Failure) 3075K->1561K(132096K), 0.0045140 secs]
[GC (Allocation Failure) 1874K->1059K(132096K), 0.0062330 secs]
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com..questions.SO23170832.parallel
# Warmup Iteration 1: [GC (Allocation Failure) 7014K->5445K(132096K), 0.0184680 secs]
[GC (Allocation Failure) 7493K->6346K(135168K), 0.0068380 secs]
[GC (Allocation Failure) 10442K->8663K(135168K), 0.0155600 secs]
[GC (Allocation Failure) 12759K->11051K(139776K), 0.0148190 secs]
[GC (Allocation Failure) 18219K->15067K(140800K), 0.0241780 secs]
[GC (Allocation Failure) 22167K->19214K(145920K), 0.0208510 secs]
[GC (Allocation Failure) 29454K->25065K(147456K), 0.0333080 secs]
[GC (Allocation Failure) 35305K->30729K(153600K), 0.0376610 secs]
[GC (Allocation Failure) 46089K->39406K(154624K), 0.0406060 secs]
[GC (Allocation Failure) 54766K->48299K(164352K), 0.0550140 secs]
[GC (Allocation Failure) 71851K->62725K(165376K), 0.0612780 secs]
[GC (Allocation Failure) 86277K->74864K(184320K), 0.0649210 secs]
[GC (Allocation Failure) 111216K->94203K(185856K), 0.0875710 secs]
[GC (Allocation Failure) 130555K->114932K(199680K), 0.1030540 secs]
[GC (Allocation Failure) 162548K->141952K(203264K), 0.1315720 secs]
[Full GC (Ergonomics) 141952K->59696K(159232K), 0.5150890 secs]
[GC (Allocation Failure) 105613K->85547K(184832K), 0.0738530 secs]
1.183 ops/s
Nota: las líneas que comienzan con #
son líneas de salida JMH normales. Todo lo demás son mensajes de GC. Esta es solo la primera de las cinco iteraciones de calentamiento, que precede a cinco iteraciones de referencia. Los mensajes del GC continuaron en la misma línea durante el resto de las iteraciones. Creo que es seguro decir que el rendimiento medido está dominado por la sobrecarga de GC y que los resultados informados no deben ser creídos.
En este punto, no está claro qué hacer. Esta es una carga de trabajo puramente sintética. Claramente implica muy poco tiempo de CPU haciendo el trabajo real en comparación con la asignación y la copia. Es difícil decir lo que realmente estás tratando de medir aquí. Un enfoque sería crear una carga de trabajo diferente que, en cierto sentido, sea más "real". Otro enfoque sería cambiar los parámetros de pila y GC para evitar GC durante la ejecución de referencia.