streams procesamiento parte operaciones libreria funcionales ejemplos datos con collection java java-8 java-stream

procesamiento - stream collection java



¿Por qué existe Collection.parallelStream() cuando.stream(). Parallel() hace lo mismo? (1)

En Java 8, la interfaz de la Colección se amplió con dos métodos que devuelven el Stream<E> : stream() , que devuelve un flujo secuencial, y parallelStream() , que devuelve un flujo posiblemente paralelo. El flujo en sí mismo también tiene un método parallel() que devuelve un flujo paralelo equivalente (ya sea mutando el flujo actual para que sea paralelo o creando un nuevo flujo).

La duplicación tiene obvias desventajas:

  • Es confuso. Una pregunta pregunta si es necesario llamar a ambos paraleloStream (). Parallel () es necesario para asegurarse de que el flujo sea paralelo , dado que parallelStream () puede devolver un flujo secuencial. ¿Por qué existe parallelStream () si no puede hacer una garantía? La otra forma también es confusa: si parallelStream () devuelve un flujo secuencial, es probable que haya una razón (por ejemplo, una estructura de datos inherentemente secuencial para la cual los flujos paralelos son una trampa de rendimiento); ¿Qué debería hacer Stream.parallel () para dicha transmisión? (UnsupportedOperationException no está permitido por la especificación de parallel ()).

  • Agregar métodos a una interfaz pone en riesgo los conflictos si una implementación existente tiene un método de nombre similar con un tipo de retorno incompatible. La adición de parallelStream () además de stream () duplica el riesgo de poca ganancia. (Tenga en cuenta que parallelStream () en un punto se llamaba en paralelo (), aunque no sé si se le cambió el nombre para evitar conflictos de nombres o por otra razón).

¿Por qué existe Collection.parallelStream () al llamar a Collection.stream ()? Parallel () hace lo mismo?


Los Javadocs para la Collection.(parallelS|s)tream() y Stream sí no responden a la pregunta, por lo que se dirige a las listas de correo para la justificación. Revisé los archivos de lambda-libs-spec-observers y encontré un hilo específicamente sobre Collection.parallelStream () y otro hilo que tocaba si java.util.Arrays debería proporcionar parallelStream () para que coincida (o en realidad, si debería ser remoto). No hubo una conclusión de una vez por todas, así que tal vez me he perdido algo de otra lista o el asunto se resolvió en una discusión privada. (Quizás Brian Goetz , uno de los directores de esta discusión, puede completar cualquier cosa que falte).

Los participantes hicieron bien sus puntos, por lo que esta respuesta es principalmente una organización de las citas relevantes, con algunas aclaraciones entre [paréntesis] , presentadas en orden de importancia (según lo interpreto).

parallelStream () cubre un caso muy común

Brian Goetz en el primer hilo, explicando por qué Collections.parallelStream() es lo suficientemente valioso para mantener incluso después de que se hayan eliminado otros métodos de fábrica de flujo paralelo:

No tenemos versiones paralelas explícitas de cada una de estas [fábricas de flujos] ; lo hicimos originalmente, y para reducir el área de superficie de la API, los recortamos en la teoría de que eliminar más de 20 métodos de la API valía la compensación del asco de la superficie y el costo de rendimiento de .intRange(...).parallel() . Pero no hicimos esa elección con la colección.

Podríamos eliminar el Collection.parallelStream() , o podríamos agregar las versiones paralelas de todos los generadores, o no podríamos hacer nada y dejarlo como está. Creo que todos son justificables en términos de diseño de API.

Me gusta el status quo, a pesar de su inconsistencia. En lugar de tener métodos de construcción de flujos 2N, tenemos N + 1, pero ese extra 1 cubre un gran número de casos, ya que es heredado por cada Colección. Así que me puedo justificar por qué vale la pena tener ese método extra 1, y por qué aceptar la inconsistencia de no ir más lejos es aceptable.

¿Están otros en desacuerdo? ¿Es N + 1 [Collections.parallelStream () only] la opción práctica aquí? ¿O deberíamos ir por la pureza de N [confiar en Stream.parallel ()] ? ¿O la conveniencia y consistencia de 2N [versiones paralelas de todas las fábricas] ? ¿O hay algunos incluso mejores N + 3 [Collections.parallelStream () más otros casos especiales] , para algunos otros casos especialmente elegidos a los que queremos darles un soporte especial?

Brian Goetz apoya esta posición en la discusión posterior sobre Arrays.parallelStream() :

Todavía me gusta mucho Collection.parallelStream; tiene enormes ventajas de capacidad de descubrimiento y ofrece un rendimiento bastante grande en el área de superficie de la API, un método más, pero proporciona valor en muchos lugares, ya que la Colección será un caso muy común de una fuente de flujo.

parallelStream () es más performante

Brian Goetz :

La versión directa [parallelStream ()] es más eficaz, ya que requiere menos ajuste (para convertir un flujo en un flujo paralelo, primero debe crear el flujo secuencial y luego transferir la propiedad de su estado a un nuevo flujo).

En respuesta al escepticismo de Kevin Bourrillion sobre si el efecto es significativo, Brian nuevamente :

Depende de lo serio que estés contando. Doug cuenta las creaciones de objetos individuales y las invocaciones virtuales en el camino hacia una operación paralela, porque hasta que comienzas a bifurcar, estás en el lado equivocado de la ley de Amdahl: todo esto es una "fracción serial" que sucede antes de que puedas realizar cualquier trabajo. que empuja su umbral de equilibrio más lejos. Por lo tanto, obtener el camino de configuración para operaciones paralelas rápido es valioso.

Doug Lea sigue , pero cubre su posición:

Las personas que tratan con el apoyo de la biblioteca paralela necesitan algún ajuste de actitud sobre tales cosas. En una máquina que pronto será típica, cada ciclo que desperdicie configurando el paralelismo le cuesta 64 ciclos. Probablemente habría tenido una reacción diferente si se necesitaran 64 creaciones de objetos para iniciar un cálculo paralelo.

Dicho esto, siempre apoyo completamente el hecho de obligar a los implementadores a trabajar más arduamente para obtener mejores API, siempre y cuando las API no descarten una implementación eficiente. Entonces, si matar a parallelStream es realmente importante, encontraremos la forma de convertir stream().parallel() Parallel stream().parallel() en un bit-flip o somesuch.

De hecho, la discusión posterior sobre Arrays.parallelStream() toma nota del menor costo de Stream.parallel () .

stream (). el estado de paralelismo () complica el futuro

En el momento de la discusión, el cambio de una secuencia de secuencial a paralelo y viceversa podría intercalarse con otras operaciones de secuencia. Brian Goetz, en nombre de Doug Lea , explica por qué el cambio de modo secuencial / paralelo puede complicar el desarrollo futuro de la plataforma Java:

Intentaré explicar por qué: porque (como los métodos con estado (clasificación, distinción, límite)) que tampoco le gustan, nos aleja cada vez más de poder expresar canales de flujo en términos de datos tradicionales -construcciones paralelas, que restringen aún más nuestra capacidad para mapearlas directamente al sustrato informático del mañana, ya sea procesadores vectoriales, FPGA, GPU o lo que sea que cocinemos.

Filtre-reduzca-reduzca los mapas de forma muy limpia a todo tipo de sustratos de computación en paralelo; filtro-paralelo-mapa-secuencial-ordenado-límite-paralelo-mapa-uniq-reduce no lo hace.

Por lo tanto, todo el diseño de la API aquí representa muchas tensiones entre facilitar la expresión de las cosas que el usuario probablemente quiera expresar, y hacerlo de una manera que podemos predecir rápidamente con modelos de costos transparentes.

Este cambio de modo se eliminó después de una discusión adicional . En la versión actual de la biblioteca, un flujo de flujo es secuencial o paralelo; última llamada a triunfos sequential() / parallel() . Además de evitar el problema de la condición de estado, este cambio también mejoró el rendimiento del uso de parallel() para configurar una tubería paralela desde una fábrica de flujos secuenciales.

La exposición de parallelStream () como ciudadano de primera clase mejora la percepción del programador de la biblioteca, lo que los lleva a escribir mejor código

Brian Goetz nuevamente , en respuesta al argumento de Tim Peierls de que Stream.parallel() permite a los programadores entender las secuencias de forma secuencial antes de ir en paralelo:

Tengo un punto de vista ligeramente diferente sobre el valor de esta intuición secuencial: veo la "expectativa secuencial" generalizada como uno de los mayores desafíos de todo este esfuerzo; la gente está constantemente trayendo su sesgo secuencial incorrecto, lo que los lleva a hacer cosas estúpidas como usar una matriz de un elemento como una forma de "engañar" al compilador "estúpido" para que les permita capturar un local mutable, o usar lambdas como argumentos para mapear ese estado de mutación que se usará durante el cálculo (de una manera no segura para subprocesos), y luego, cuando se señale que lo que están haciendo, se encoge de hombros y dice "sí, pero no lo estoy haciendo en paralelo."

Hemos hecho muchas concesiones de diseño para combinar flujos secuenciales y paralelos. El resultado, creo, es limpio y aumentará las posibilidades de que la biblioteca siga siendo útil en más de 10 años, pero no me gusta especialmente la idea de alentar a las personas a pensar que esta es una biblioteca secuencial con algunas bolsas paralelas clavadas. en el lado.