From 5666c409872aa9f6c2db520f780a6ca78680a6b5 Mon Sep 17 00:00:00 2001 From: Santiago Basulto Date: Wed, 4 Apr 2012 12:27:57 -0300 Subject: [PATCH 1/3] Starting parallel collection overviews translation --- overviews/parallel-collections/es/overview.md | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 overviews/parallel-collections/es/overview.md diff --git a/overviews/parallel-collections/es/overview.md b/overviews/parallel-collections/es/overview.md new file mode 100644 index 0000000000..d0f379d6d3 --- /dev/null +++ b/overviews/parallel-collections/es/overview.md @@ -0,0 +1,244 @@ +--- +layout: overview-large +title: Overview + +disqus: true + +partof: parallel-collections +num: 1 +languages: [ja] +--- + +**Autores originales: Aleksandar Prokopec, Heather Miller** + +**Traducción y arreglos: Santiago Basulto** + +## Motivación + +En el medio del cambio en los recientes años de los fabricantes de procesadores de arquitecturas simples a arquitecturas multi-nucleo, tanto el ámbito académico, como el industrial coinciden que la _Programación Paralela_ sigue siendo un gran desafío. + +Las Colecciones Paralelizadas (Parallel collections, en inglés) fueron incluidas en la librería del lenguaje Scala en un esfuerzo de facilitar la programación paralela al abstraer a los usuarios de detalles de paralelización de bajo nivel, mientras se provee con una abstracción de alto nivel, simple y familiar. La esperanza era, y sigue siendo, que el paralelismo implícito detrás de una abstracción de colecciones (como lo es el actual framework de colecciones del lenguaje) acercara la ejecución paralela confiable, un poco más al trabajo diario de los desarrolladores. + +La idea es simple: las colecciones son abstracciones de programación ficientemente entendidas y a su vez son frecuentemente usadas. Dada su regularidad, es posible que sean paralelizadas eficiente y transparentemente. Al permitirle al usuario intercambiar colecciones secuenciales por aquellas que son operadas en paralelo, las colecciones paralelizadas de Scala dan un gran paso hacia la posibilidad de que el paralelismo sea introducido cada vez más frecuentemente en nuestro código. + +Veamos el siguiente ejemplo secuencial, donde realizamos una operación monádica en una colección lo suficientemente grande. + + val list = (1 to 10000).toList + list.map(_ + 42) + +Para realizar la misma operación en paralelo, lo único que devemos incluir, es la invocación al método `par` en la colección secuencial `list`. Después de eso, es posible utilizar la misma colección paralelizada de la misma manera que normalmente la usariamos si fuera una colección secuencial. El ejemplo superior puede ser paralelizado al hacer simplemente lo siguiente: + + list.par.map(_ + 42) + +El diseño de la librería de colecciones paralelizadas de Scala está inspirada y fuertemente integrada con la librería estandar de colecciones (secuenciales) del lenguaje (introducida en la versión 2.8). Se provee te una contraparte paralelizada a un número importante de estructuras de datos de la librería de colecciones (secuenciales) de Scala, incluyendo: + +* `ParArray` +* `ParVector` +* `mutable.ParHashMap` +* `mutable.ParHashSet` +* `immutable.ParHashMap` +* `immutable.ParHashSet` +* `ParRange` +* `ParTrieMap` (`collection.concurrent.TrieMap`s are new in 2.10) + +Además de una arquitectura común, la librería de colecciones paralelizadas de Scala también comparte la _extensibilidad_ con la librería de colecciones secuenciales. Es decir, de la misma manera que los usuarios pueden integrar sus propios tipos de tipos de colecciones de la librería normal de colecciones secuenciales, pueden realizarlo con la librería de colecciones paralelizadas, heredando automáticamente todas las operaciones paralelas disponibles en las demás colecciones paralelizadas de la librería estandar. + +## Algunos Ejemplos + +To attempt to illustrate the generality and utility of parallel collections, +we provide a handful of simple example usages, all of which are transparently +executed in parallel. + +De forma de ilustrar la generalidad y utilidad de las colecciones paralelizadas, proveemos un conjunto de ejemplos de uso útiles, todos ellos siendo ejecutados en paralelo de forma totalmente transparente al usuario. + +_Nota:_ Algunos de los siguientes ejemplos operan en colecciones pequeñas, lo cual no es recomendado. Son provistos como ejemplo para ilustrar solamente el propósito. Como una regla heurística general, los incrementos en velocidad de ejecución comienzan a ser notados cuando el tamaño de la colección es lo suficientemente grande, tipicamente algunos cuantos miles de elementos. (Para más información en la relación entre tamaño de una coleccion paralelizada y su performance, por favor véase [appropriate subsection]({{ site.baseurl}}/overviews/parallel-collections/performance.html#how_big_should_a_collection_be_to_go_parallel) en la sección [performance]({{ site.baseurl }}/overviews/parallel-collections/performance.html) (en inglés). + +#### map + +Usando un `map` paralelizado para transformar una colección de elementos tipo `String` a todos caracteres en mayúscula: + + scala> val apellidos = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par + apellidos: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) + + scala> apellidos.map(_.toUpperCase) + res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(SMITH, JONES, FRANKENSTEIN, BACH, JACKSON, RODIN) + +#### fold + +Sumatoria mediante `fold` en un `ParArray`: + + scala> val parArray = (1 to 1000000).toArray.par + parArray: scala.collection.parallel.mutable.ParArray[Int] = ParArray(1, 2, 3, ... + + scala> parArray.fold(0)(_ + _) + res0: Int = 1784293664 + +#### filtrando + + +Usando un filtrado mediante `filter` paralelizado para seleccionar los apellidos que alfabéticamente preceden la letra "K": + + scala> val apellidos = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par + apellidos: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) + + scala> apellidos.filter(_.head >= 'J') + res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Jackson, Rodin) + +## Creación de colecciones paralelizadas + +Las colecciones paralelizadas están pensadas para ser usadas exactamente de la misma manera que las colecciones secuenciales --la única diferencia notoria es cómo _obtener_ una colección paralelizada. + +Generalmente se tienen dos opciones para la creación de colecciones paralelizadas: + +Primero al utilizar la palabra clave `new` y una sentencia de importación apropiada: + + import scala.collection.parallel.immutable.ParVector + val pv = new ParVector[Int] + +Segundo, al _convertir_ desde una colección secuencial: + + val pv = Vector(1,2,3,4,5,6,7,8,9).par + +What's important to expand upon here are these conversion methods-- sequential +collections can be converted to parallel collections by invoking the +sequential collection's `par` method, and likewise, parallel collections can +be converted to sequential collections by invoking the parallel collection's +`seq` method. + +_Of Note:_ Collections that are inherently sequential (in the sense that the +elements must be accessed one after the other), like lists, queues, and +streams, are converted to their parallel counterparts by copying the elements +into a similar parallel collection. An example is `List`-- it's converted into +a standard immutable parallel sequence, which is a `ParVector`. Of course, the +copying required for these collection types introduces an overhead not +incurred by any other collection types, like `Array`, `Vector`, `HashMap`, etc. + +For more information on conversions on parallel collections, see the +[conversions]({{ site.baseurl }}/overviews/parallel-collections/converesions.html) +and [concrete parallel collection classes]({{ site.baseurl }}/overviews/parallel-collections/concrete-parallel-collections.html) +sections of thise guide. + +## Semantics + +While the parallel collections abstraction feels very much the same as normal +sequential collections, it's important to note that its semantics differs, +especially with regards to side-effects and non-associative operations. + +In order to see how this is the case, first, we visualize _how_ operations are +performed in parallel. Conceptually, Scala's parallel collections framework +parallelizes an operation on a parallel collection by recursively "splitting" +a given collection, applying an operation on each partition of the collection +in parallel, and re-"combining" all of the results that were completed in +parallel. + +These concurrent, and "out-of-order" semantics of parallel collections lead to +the following two implications: + +1. **Side-effecting operations can lead to non-determinism** +2. **Non-associative operations lead to non-determinism** + +### Side-Effecting Operations + +Given the _concurrent_ execution semantics of the parallel collections +framework, operations performed on a collection which cause side-effects +should generally be avoided, in order to maintain determinism. A simple +example is by using an accessor method, like `foreach` to increment a `var` +declared outside of the closure which is passed to `foreach`. + + scala> var sum = 0 + sum: Int = 0 + + scala> val list = (1 to 1000).toList.par + list: scala.collection.parallel.immutable.ParSeq[Int] = ParVector(1, 2, 3,… + + scala> list.foreach(sum += _); sum + res01: Int = 467766 + + scala> var sum = 0 + sum: Int = 0 + + scala> list.foreach(sum += _); sum + res02: Int = 457073 + + scala> var sum = 0 + sum: Int = 0 + + scala> list.foreach(sum += _); sum + res03: Int = 468520 + +Here, we can see that each time `sum` is reinitialized to 0, and `foreach` is +called again on `list`, `sum` holds a different value. The source of this +non-determinism is a _data race_-- concurrent reads/writes to the same mutable +variable. + +In the above example, it's possible for two threads to read the _same_ value +in `sum`, to spend some time doing some operation on that value of `sum`, and +then to attempt to write a new value to `sum`, potentially resulting in an +overwrite (and thus, loss) of a valuable result, as illustrated below: + + ThreadA: read value in sum, sum = 0 value in sum: 0 + ThreadB: read value in sum, sum = 0 value in sum: 0 + ThreadA: increment sum by 760, write sum = 760 value in sum: 760 + ThreadB: increment sum by 12, write sum = 12 value in sum: 12 + +The above example illustrates a scenario where two threads read the same +value, `0`, before one or the other can sum `0` with an element from their +partition of the parallel collection. In this case, `ThreadA` reads `0` and +sums it with its element, `0+760`, and in the case of `ThreadB`, sums `0` with +its element, `0+12`. After computing their respective sums, they each write +their computed value in `sum`. Since `ThreadA` beats `ThreadB`, it writes +first, only for the value in `sum` to be overwritten shortly after by +`ThreadB`, in effect completely overwriting (and thus losing) the value `760`. + +### Non-Associative Operations + +Given this _"out-of-order"_ semantics, also must be careful to perform only +associative operations in order to avoid non-determinism. That is, given a +parallel collection, `pcoll`, one should be sure that when invoking a +higher-order function on `pcoll`, such as `pcoll.reduce(func)`, the order in +which `func` is applied to the elements of `pcoll` can be arbitrary. A simple, +but obvious example is a non-associative operation such as subtraction: + + scala> val list = (1 to 1000).toList.par + list: scala.collection.parallel.immutable.ParSeq[Int] = ParVector(1, 2, 3,… + + scala> list.reduce(_-_) + res01: Int = -228888 + + scala> list.reduce(_-_) + res02: Int = -61000 + + scala> list.reduce(_-_) + res03: Int = -331818 + +In the above example, we take a `ParVector[Int]`, invoke `reduce`, and pass to +it `_-_`, which simply takes two unnamed elements, and subtracts the first +from the second. Due to the fact that the parallel collections framework spawns +threads which, in effect, independently perform `reduce(_-_)` on different +sections of the collection, the result of two runs of `reduce(_-_)` on the +same collection will not be the same. + +_Note:_ Often, it is thought that, like non-associative operations, non-commutative +operations passed to a higher-order function on a parallel +collection likewise result in non-deterministic behavior. This is not the +case, a simple example is string concatenation-- an associative, but non- +commutative operation: + + scala> val strings = List("abc","def","ghi","jk","lmnop","qrs","tuv","wx","yz").par + strings: scala.collection.parallel.immutable.ParSeq[java.lang.String] = ParVector(abc, def, ghi, jk, lmnop, qrs, tuv, wx, yz) + + scala> val alphabet = strings.reduce(_++_) + alphabet: java.lang.String = abcdefghijklmnopqrstuvwxyz + +The _"out of order"_ semantics of parallel collections only means that +the operation will be executed out of order (in a _temporal_ sense. That is, +non-sequentially), it does not mean that the result will be +re-"*combined*" out of order (in a _spatial_ sense). On the contrary, results +will generally always be reassembled _in order_-- that is, a parallel collection +broken into partitions A, B, C, in that order, will be reassembled once again +in the order A, B, C. Not some other arbitrary order like B, C, A. + +For more on how parallel collections split and combine operations on different +parallel collection types, see the [Architecture]({{ site.baseurl }}/overviews +/parallel-collections/architecture.html) section of this guide. + From 66c59a1ebba8182554528bf37aa384488537b598 Mon Sep 17 00:00:00 2001 From: Santiago Basulto Date: Fri, 6 Apr 2012 00:02:23 -0300 Subject: [PATCH 2/3] More parallel-collections/overview translations --- .../parallel-collections/architecture.md | 116 ++++++ .../concrete-parallel-collections.md | 270 ++++++++++++++ .../parallel-collections/configuration.md | 82 +++++ .../parallel-collections/conversions.md | 80 +++++ es/overviews/parallel-collections/ctries.md | 166 +++++++++ .../custom-parallel-collections.md | 332 ++++++++++++++++++ .../parallel-collections/es/overview.md | 244 +++++++++++++ es/overviews/parallel-collections/overview.md | 219 ++++++++++++ .../parallel-collections/performance.md | 284 +++++++++++++++ 9 files changed, 1793 insertions(+) create mode 100644 es/overviews/parallel-collections/architecture.md create mode 100644 es/overviews/parallel-collections/concrete-parallel-collections.md create mode 100644 es/overviews/parallel-collections/configuration.md create mode 100644 es/overviews/parallel-collections/conversions.md create mode 100644 es/overviews/parallel-collections/ctries.md create mode 100644 es/overviews/parallel-collections/custom-parallel-collections.md create mode 100644 es/overviews/parallel-collections/es/overview.md create mode 100644 es/overviews/parallel-collections/overview.md create mode 100644 es/overviews/parallel-collections/performance.md diff --git a/es/overviews/parallel-collections/architecture.md b/es/overviews/parallel-collections/architecture.md new file mode 100644 index 0000000000..15c9a79a6b --- /dev/null +++ b/es/overviews/parallel-collections/architecture.md @@ -0,0 +1,116 @@ +--- +layout: overview-large +title: Architecture of the Parallel Collections Library + +disqus: true + +partof: parallel-collections +num: 5 +languages: [ja] +--- + +Like the normal, sequential collections library, Scala's parallel collections +library contains a large number of collection operations which exist uniformly +on many different parallel collection implementations. And like the sequential +collections library, Scala's parallel collections library seeks to prevent +code duplication by likewise implementing most operations in terms of parallel +collection "templates" which need only be defined once and can be flexibly +inherited by many different parallel collection implementations. + +The benefits of this approach are greatly eased **maintenance** and +**extensibility**. In the case of maintenance-- by having a single +implementation of a parallel collections operation inherited by all parallel +collections, maintenance becomes easier and more robust; bug fixes propagate +down the class hierarchy, rather than needing implementations to be +duplicated. For the same reasons, the entire library becomes easier to +extend-- new collection classes can simply inherit most of their operations. + +## Core Abstractions + +The aforementioned "template" traits implement most parallel operations in +terms of two core abstractions-- `Splitter`s and `Combiner`s. + +### Splitters + +The job of a `Splitter`, as its name suggests, is to split a parallel +collection into a non-trival partition of its elements. The basic idea is to +split the collection into smaller parts until they are small enough to be +operated on sequentially. + + trait Splitter[T] extends Iterator[T] { + def split: Seq[Splitter[T]] + } + +Interestingly, `Splitter`s are implemented as `Iterator`s, meaning that apart +from splitting, they are also used by the framework to traverse a parallel +collection (that is, they inherit standard methods on `Iterator`s such as +`next` and `hasNext`.) What's unique about this "splitting iterator" is that, +its `split` method splits `this` (again, a `Splitter`, a type of `Iterator`) +further into additional `Splitter`s which each traverse over **disjoint** +subsets of elements of the whole parallel collection. And similar to normal +`Iterator`s, a `Splitter` is invalidated after its `split` method is invoked. + +In general, collections are partitioned using `Splitter`s into subsets of +roughly the same size. In cases where more arbitrarily-sized partions are +required, in particular on parallel sequences, a `PreciseSplitter` is used, +which inherits `Splitter` and additionally implements a precise split method, +`psplit`. + +### Combiners + +`Combiner`s can be thought of as a generalized `Builder`, from Scala's sequential +collections library. Each parallel collection provides a separate `Combiner`, +in the same way that each sequential collection provides a `Builder`. + +While in the case of sequential collections, elements can be added to a +`Builder`, and a collection can be produced by invoking the `result` method, +in the case of parallel collections, a `Combiner` has a method called +`combine` which takes another `Combiner` and produces a new `Combiner` that +contains the union of both's elements. After `combine` has been invoked, both +`Combiner`s become invalidated. + + trait Combiner[Elem, To] extends Builder[Elem, To] { + def combine(other: Combiner[Elem, To]): Combiner[Elem, To] + } + +The two type parameters `Elem` and `To` above simply denote the element type +and the type of the resulting collection, respectively. + +_Note:_ Given two `Combiner`s, `c1` and `c2` where `c1 eq c2` is `true` +(meaning they're the same `Combiner`), invoking `c1.combine(c2)` always does +nothing and simpy returns the receiving `Combiner`, `c1`. + +## Hierarchy + +Scala's parallel collection's draws much inspiration from the design of +Scala's (sequential) collections library-- as a matter of fact, it mirrors the +regular collections framework's corresponding traits, as shown below. + +[]({{ site.baseurl }}/resources/images/parallel-collections-hierarchy.png) + +
Hierarchy of Scala's Collections and Parallel Collections Libraries
+
+ +The goal is of course to integrate parallel collections as tightly as possible +with sequential collections, so as to allow for straightforward substitution +of sequential and parallel collections. + +In order to be able to have a reference to a collection which may be either +sequential or parallel (such that it's possible to "toggle" between a parallel +collection and a sequential collection by invoking `par` and `seq`, +respectively), there has to exist a common supertype of both collection types. +This is the origin of the "general" traits shown above, `GenTraversable`, +`GenIterable`, `GenSeq`, `GenMap` and `GenSet`, which don't guarantee in-order +or one-at-a-time traversal. Corresponding sequential or parallel traits +inherit from these. For example, a `ParSeq` and `Seq` are both subtypes of a +general sequence `GenSeq`, but they are in no inheritance relationship with +respect to each other. + +For a more detailed discussion of hierarchy shared between sequential and +parallel collections, see the technical report. \[[1][1]\] + +## References + +1. [On a Generic Parallel Collection Framework, Aleksandar Prokopec, Phil Bawgell, Tiark Rompf, Martin Odersky, June 2011][1] + +[1]: http://infoscience.epfl.ch/record/165523/files/techrep.pdf "flawed-benchmark" diff --git a/es/overviews/parallel-collections/concrete-parallel-collections.md b/es/overviews/parallel-collections/concrete-parallel-collections.md new file mode 100644 index 0000000000..0f987da2bf --- /dev/null +++ b/es/overviews/parallel-collections/concrete-parallel-collections.md @@ -0,0 +1,270 @@ +--- +layout: overview-large +title: Concrete Parallel Collection Classes + +disqus: true + +partof: parallel-collections +num: 2 +languages: [ja] +--- + +## Parallel Array + +A [ParArray](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/parallel/mutable/ParArray.html) +sequence holds a linear, +contiguous array of elements. This means that the elements can be accessed and +updated efficiently by modifying the underlying array. Traversing the +elements is also very efficient for this reason. Parallel arrays are like +arrays in the sense that their size is constant. + + scala> val pa = scala.collection.parallel.mutable.ParArray.tabulate(1000)(x => 2 * x + 1) + pa: scala.collection.parallel.mutable.ParArray[Int] = ParArray(1, 3, 5, 7, 9, 11, 13,... + + scala> pa reduce (_ + _) + res0: Int = 1000000 + + scala> pa map (x => (x - 1) / 2) + res1: scala.collection.parallel.mutable.ParArray[Int] = ParArray(0, 1, 2, 3, 4, 5, 6, 7,... + +Internally, splitting a parallel array +[splitter]({{ site.baseurl }}/overviews/parallel-collections/architecture.html#core_abstractions) +amounts to creating two new splitters with their iteration indices updated. +[Combiners]({{ site.baseurl }}/overviews/parallel-collections/architecture.html#core_abstractions) +are slightly more involved.Since for most transformer methods (e.g. `flatMap`, `filter`, `takeWhile`, +etc.) we don't know the number of elements (and hence, the array size) in +advance, each combiner is essentially a variant of an array buffer with an +amortized constant time `+=` operation. Different processors add elements to +separate parallel array combiners, which are then combined by chaining their +internal arrays. The underlying array is only allocated and filled in parallel +after the total number of elements becomes known. For this reason, transformer +methods are slightly more expensive than accessor methods. Also, note that the +final array allocation proceeds sequentially on the JVM, so this can prove to +be a sequential bottleneck if the mapping operation itself is very cheap. + +By calling the `seq` method, parallel arrays are converted to `ArraySeq` +collections, which are their sequential counterparts. This conversion is +efficient, and the `ArraySeq` is backed by the same underlying array as the +parallel array it was obtained from. + + +## Parallel Vector + +A [ParVector](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/parallel/immutable/ParVector.html) +is an immutable sequence with a low-constant factor logarithmic access and +update time. + + scala> val pv = scala.collection.parallel.immutable.ParVector.tabulate(1000)(x => x) + pv: scala.collection.parallel.immutable.ParVector[Int] = ParVector(0, 1, 2, 3, 4, 5, 6, 7, 8, 9,... + + scala> pv filter (_ % 2 == 0) + res0: scala.collection.parallel.immutable.ParVector[Int] = ParVector(0, 2, 4, 6, 8, 10, 12, 14, 16, 18,... + +Immutable vectors are represented by 32-way trees, so +[splitter]({{ site.baseurl }}/overviews/parallel-collections/architecture.html#core_abstractions)s +are split byassigning subtrees to each splitter. +[Combiners]({{ site.baseurl }}/overviews/parallel-collections/architecture.html#core_abstractions) +currently keep a vector of +elements and are combined by lazily copying the elements. For this reason, +transformer methods are less scalable than those of a parallel array. Once the +vector concatenation operation becomes available in a future Scala release, +combiners will be combined using concatenation and transformer methods will +become much more efficient. + +Parallel vector is a parallel counterpart of the sequential +[Vector](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/immutable/Vector.html), +so conversion between the two takes constant time. + + +## Parallel Range + +A [ParRange](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/parallel/immutable/ParRange.html) +is an ordered sequence of elements equally spaced apart. A parallel range is +created in a similar way as the sequential +[Range](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/immutable/Range.html): + + scala> 1 to 3 par + res0: scala.collection.parallel.immutable.ParRange = ParRange(1, 2, 3) + + scala> 15 to 5 by -2 par + res1: scala.collection.parallel.immutable.ParRange = ParRange(15, 13, 11, 9, 7, 5) + +Just as sequential ranges have no builders, parallel ranges have no +[combiner]({{ site.baseurl }}/overviews/parallel-collections/architecture.html#core_abstractions)s. +Mapping the elements of a parallel range produces a parallel vector. +Sequential ranges and parallel ranges can be converted efficiently one from +another using the `seq` and `par` methods. + + +## Parallel Hash Tables + +Parallel hash tables store their elements in an underlying array and place +them in the position determined by the hash code of the respective element. +Parallel mutable hash sets +([mutable.ParHashSet](http://www.scala-lang.org/api/{{ site.scala-version}}/scala/collection/parallel/mutable/ParHashSet.html)) +and parallel mutable hash maps +([mutable.ParHashMap](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/parallel/mutable/ParHashMap.html)) +are based on hash tables. + + scala> val phs = scala.collection.parallel.mutable.ParHashSet(1 until 2000: _*) + phs: scala.collection.parallel.mutable.ParHashSet[Int] = ParHashSet(18, 327, 736, 1045, 773, 1082,... + + scala> phs map (x => x * x) + res0: scala.collection.parallel.mutable.ParHashSet[Int] = ParHashSet(2181529, 2446096, 99225, 2585664,... + +Parallel hash table combiners sort elements into buckets according to their +hashcode prefix. They are combined by simply concatenating these buckets +together. Once the final hash table is to be constructed (i.e. combiner +`result` method is called), the underlying array is allocated and the elements +from different buckets are copied in parallel to different contiguous segments +of the hash table array. + +Sequential hash maps and hash sets can be converted to their parallel variants +using the `par` method. Parallel hash tables internally require a size map +which tracks the number of elements in different chunks of the hash table. +What this means is that the first time that a sequential hash table is +converted into a parallel hash table, the table is traversed and the size map +is created - for this reason, the first call to `par` takes time linear in the +size of the hash table. Further modifications to the hash table maintain the +state of the size map, so subsequent conversions using `par` and `seq` have +constant complexity. Maintenance of the size map can be turned on and off +using the `useSizeMap` method of the hash table. Importantly, modifications in +the sequential hash table are visible in the parallel hash table, and vice +versa. + + +## Parallel Hash Tries + +Parallel hash tries are a parallel counterpart of the immutable hash tries, +which are used to represent immutable sets and maps efficiently. They are +supported by classes +[immutable.ParHashSet](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/parallel/immutable/ParHashSet.html) +and +[immutable.ParHashMap](http://www.scala-lang.org/api/{{ site.scala-version}}/scala/collection/parallel/immutable/ParHashMap.html). + + scala> val phs = scala.collection.parallel.immutable.ParHashSet(1 until 1000: _*) + phs: scala.collection.parallel.immutable.ParHashSet[Int] = ParSet(645, 892, 69, 809, 629, 365, 138, 760, 101, 479,... + + scala> phs map { x => x * x } sum + res0: Int = 332833500 + +Similar to parallel hash tables, parallel hash trie +[combiners]({{ site.baseurl }}/overviews/parallel-collections/architecture.html#core_abstractions) +pre-sort the +elements into buckets and construct the resulting hash trie in parallel by +assigning different buckets to different processors, which construct the +subtries independently. + +Parallel hash tries can be converted back and forth to sequential hash tries +by using the `seq` and `par` method in constant time. + + +## Parallel Concurrent Tries + +A [concurrent.TrieMap](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/concurrent/TrieMap.html) +is a concurrent thread-safe map, whereas a +[mutable.ParTrieMap](http://www.scala-lang.org/api/{{ site.scala-version}}/scala/collection/parallel/mutable/ParTrieMap.html) +is its parallel counterpart. While most concurrent data structures do not guarantee +consistent traversal if the the data structure is modified during traversal, +Ctries guarantee that updates are only visible in the next iteration. This +means that you can mutate the concurrent trie while traversing it, like in the +following example which outputs square roots of number from 1 to 99: + + scala> val numbers = scala.collection.parallel.mutable.ParTrieMap((1 until 100) zip (1 until 100): _*) map { case (k, v) => (k.toDouble, v.toDouble) } + numbers: scala.collection.parallel.mutable.ParTrieMap[Double,Double] = ParTrieMap(0.0 -> 0.0, 42.0 -> 42.0, 70.0 -> 70.0, 2.0 -> 2.0,... + + scala> while (numbers.nonEmpty) { + | numbers foreach { case (num, sqrt) => + | val nsqrt = 0.5 * (sqrt + num / sqrt) + | numbers(num) = nsqrt + | if (math.abs(nsqrt - sqrt) < 0.01) { + | println(num, nsqrt) + | numbers.remove(num) + | } + | } + | } + (1.0,1.0) + (2.0,1.4142156862745097) + (7.0,2.64576704419029) + (4.0,2.0000000929222947) + ... + + +[Combiners]({{ site.baseurl }}/overviews/parallel-collections/architecture.html#core_abstractions) +are implemented as `TrieMap`s under the hood-- since this is a +concurrent data structure, only one combiner is constructed for the entire +transformer method invocation and shared by all the processors. + +As with all parallel mutable collections, `TrieMap`s and parallel `ParTrieMap`s obtained +by calling `seq` or `par` methods are backed by the same store, so +modifications in one are visible in the other. Conversions happen in constant +time. + + +## Performance characteristics + +Performance characteristics of sequence types: + +| | head | tail | apply | update| prepend | append | insert | +| -------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| `ParArray` | C | L | C | C | L | L | L | +| `ParVector` | eC | eC | eC | eC | eC | eC | - | +| `ParRange` | C | C | C | - | - | - | - | + +Performance characteristics of set and map types: + +| | lookup | add | remove | +| -------- | ---- | ---- | ---- | +| **immutable** | | | | +| `ParHashSet`/`ParHashMap`| eC | eC | eC | +| **mutable** | | | | +| `ParHashSet`/`ParHashMap`| C | C | C | +| `ParTrieMap` | eC | eC | eC | + + +### Key + +The entries in the above two tables are explained as follows: + +| | | +| --- | ---- | +| **C** | The operation takes (fast) constant time. | +| **eC** | The operation takes effectively constant time, but this might depend on some assumptions such as maximum length of a vector or distribution of hash keys.| +| **aC** | The operation takes amortized constant time. Some invocations of the operation might take longer, but if many operations are performed on average only constant time per operation is taken. | +| **Log** | The operation takes time proportional to the logarithm of the collection size. | +| **L** | The operation is linear, that is it takes time proportional to the collection size. | +| **-** | The operation is not supported. | + +The first table treats sequence types--both immutable and mutable--with the following operations: + +| | | +| --- | ---- | +| **head** | Selecting the first element of the sequence. | +| **tail** | Producing a new sequence that consists of all elements except the first one. | +| **apply** | Indexing. | +| **update** | Functional update (with `updated`) for immutable sequences, side-effecting update (with `update` for mutable sequences. | +| **prepend**| Adding an element to the front of the sequence. For immutable sequences, this produces a new sequence. For mutable sequences it modified the existing sequence. | +| **append** | Adding an element and the end of the sequence. For immutable sequences, this produces a new sequence. For mutable sequences it modified the existing sequence. | +| **insert** | Inserting an element at an arbitrary position in the sequence. This is only supported directly for mutable sequences. | + +The second table treats mutable and immutable sets and maps with the following operations: + +| | | +| --- | ---- | +| **lookup** | Testing whether an element is contained in set, or selecting a value associated with a key. | +| **add** | Adding a new element to a set or key/value pair to a map. | +| **remove** | Removing an element from a set or a key from a map. | +| **min** | The smallest element of the set, or the smallest key of a map. | + + + + + + + + + + + + + diff --git a/es/overviews/parallel-collections/configuration.md b/es/overviews/parallel-collections/configuration.md new file mode 100644 index 0000000000..2618eef12e --- /dev/null +++ b/es/overviews/parallel-collections/configuration.md @@ -0,0 +1,82 @@ +--- +layout: overview-large +title: Configuring Parallel Collections + +disqus: true + +partof: parallel-collections +num: 7 +languages: [ja] +--- + +## Task support + +Parallel collections are modular in the way operations are scheduled. Each +parallel collection is parametrized with a task support object which is +responsible for scheduling and load-balancing tasks to processors. + +The task support object internally keeps a reference to a thread pool +implementation and decides how and when tasks are split into smaller tasks. To +learn more about the internals of how exactly this is done, see the tech +report \[[1][1]\]. + +There are currently a few task support implementations available for parallel +collections. The `ForkJoinTaskSupport` uses a fork-join pool internally and is +used by default on JVM 1.6 or greater. The less efficient +`ThreadPoolTaskSupport` is a fallback for JVM 1.5 and JVMs that do not support +the fork join pools. The `ExecutionContextTaskSupport` uses the default +execution context implementation found in `scala.concurrent`, and it reuses +the thread pool used in `scala.concurrent` (this is either a fork join pool or +a thread pool executor, depending on the JVM version). The execution context +task support is set to each parallel collection by default, so parallel +collections reuse the same fork-join pool as the future API. + +Here is a way to change the task support of a parallel collection: + + scala> import scala.collection.parallel._ + import scala.collection.parallel._ + + scala> val pc = mutable.ParArray(1, 2, 3) + pc: scala.collection.parallel.mutable.ParArray[Int] = ParArray(1, 2, 3) + + scala> pc.tasksupport = new ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(2)) + pc.tasksupport: scala.collection.parallel.TaskSupport = scala.collection.parallel.ForkJoinTaskSupport@4a5d484a + + scala> pc map { _ + 1 } + res0: scala.collection.parallel.mutable.ParArray[Int] = ParArray(2, 3, 4) + +The above sets the parallel collection to use a fork-join pool with +parallelism level 2. To set the parallel collection to use a thread pool +executor: + + scala> pc.tasksupport = new ThreadPoolTaskSupport() + pc.tasksupport: scala.collection.parallel.TaskSupport = scala.collection.parallel.ThreadPoolTaskSupport@1d914a39 + + scala> pc map { _ + 1 } + res1: scala.collection.parallel.mutable.ParArray[Int] = ParArray(2, 3, 4) + +When a parallel collection is serialized, the task support field is omitted +from serialization. When deserializing a parallel collection, the task support +field is set to the default value-- the execution context task support. + +To implement a custom task support, extend the `TaskSupport` trait and +implement the following methods: + + def execute[R, Tp](task: Task[R, Tp]): () => R + + def executeAndWaitResult[R, Tp](task: Task[R, Tp]): R + + def parallelismLevel: Int + +The `execute` method schedules a task asynchronously and returns a future to +wait on the result of the computation. The `executeAndWait` method does the +same, but only returns when the task is completed. The `parallelismLevel` +simply returns the targeted number of cores that the task support uses to +schedule tasks. + + +## References + +1. [On a Generic Parallel Collection Framework, June 2011][1] + + [1]: http://infoscience.epfl.ch/record/165523/files/techrep.pdf "parallel-collections" diff --git a/es/overviews/parallel-collections/conversions.md b/es/overviews/parallel-collections/conversions.md new file mode 100644 index 0000000000..3801f5f486 --- /dev/null +++ b/es/overviews/parallel-collections/conversions.md @@ -0,0 +1,80 @@ +--- +layout: overview-large +title: Parallel Collection Conversions + +disqus: true + +partof: parallel-collections +num: 3 +languages: [ja] +--- + +## Converting between sequential and parallel collections + +Every sequential collection can be converted to its parallel variant +using the `par` method. Certain sequential collections have a +direct parallel counterpart. For these collections the conversion is +efficient-- it occurs in constant time, since both the sequential and +the parallel collection have the same data-structural representation +(one exception is mutable hash maps and hash sets which are slightly +more expensive to convert the first time `par` is called, but +subsequent invocations of `par` take constant time). It should be +noted that for mutable collections, changes in the sequential collection are +visible in its parallel counterpart if they share the underlying data-structure. + +| Sequential | Parallel | +| ------------- | -------------- | +| **mutable** | | +| `Array` | `ParArray` | +| `HashMap` | `ParHashMap` | +| `HashSet` | `ParHashSet` | +| `TrieMap` | `ParTrieMap` | +| **immutable** | | +| `Vector` | `ParVector` | +| `Range` | `ParRange` | +| `HashMap` | `ParHashMap` | +| `HashSet` | `ParHashSet` | + +Other collections, such as lists, queues or streams, are inherently sequential +in the sense that the elements must be accessed one after the other. These +collections are converted to their parallel variants by copying the elements +into a similar parallel collection. For example, a functional list is +converted into a standard immutable parallel sequence, which is a parallel +vector. + +Every parallel collection can be converted to its sequential variant +using the `seq` method. Converting a parallel collection to a +sequential collection is always efficient-- it takes constant +time. Calling `seq` on a mutable parallel collection yields a +sequential collection which is backed by the same store-- updates to +one collection will be visible in the other one. + + +## Converting between different collection types + +Orthogonal to converting between sequential and parallel collections, +collections can be converted between different collection types. For +example, while calling `toSeq` converts a sequential set to a +sequential sequence, calling `toSeq` on a parallel set converts it to +a parallel sequence. The general rule is that if there is a +parallel version of `X`, then the `toX` method converts the collection +into a `ParX` collection. + +Here is a summary of all conversion methods: + +| Method | Return Type | +| -------------- | -------------- | +| `toArray` | `Array` | +| `toList` | `List` | +| `toIndexedSeq` | `IndexedSeq` | +| `toStream` | `Stream` | +| `toIterator` | `Iterator` | +| `toBuffer` | `Buffer` | +| `toTraversable`| `GenTraverable`| +| `toIterable` | `ParIterable` | +| `toSeq` | `ParSeq` | +| `toSet` | `ParSet` | +| `toMap` | `ParMap` | + + + diff --git a/es/overviews/parallel-collections/ctries.md b/es/overviews/parallel-collections/ctries.md new file mode 100644 index 0000000000..01209d70bc --- /dev/null +++ b/es/overviews/parallel-collections/ctries.md @@ -0,0 +1,166 @@ +--- +layout: overview-large +title: Concurrent Tries + +disqus: true + +partof: parallel-collections +num: 4 +languages: [ja] +--- + +Most concurrent data structures do not guarantee consistent +traversal if the the data structure is modified during traversal. +This is, in fact, the case with most mutable collections, too. +Concurrent tries are special in the sense that they allow you to modify +the trie being traversed itself. The modifications are only visible in the +subsequent traversal. This holds both for sequential concurrent tries and their +parallel counterparts. The only difference between the two is that the +former traverses its elements sequentially, whereas the latter does so in +parallel. + +This is a nice property that allows you to write some algorithms more +easily. Typically, these are algorithms that process a dataset of elements +iteratively, in which different elements need a different number of +iterations to be processed. + +The following example computes the square roots of a set of numbers. Each iteration +iteratively updates the square root value. Numbers whose square roots converged +are removed from the map. + + case class Entry(num: Double) { + var sqrt = num + } + + val length = 50000 + + // prepare the list + val entries = (1 until length) map { num => Entry(num.toDouble) } + val results = ParTrieMap() + for (e <- entries) results += ((e.num, e)) + + // compute square roots + while (results.nonEmpty) { + for ((num, e) <- results) { + val nsqrt = 0.5 * (e.sqrt + e.num / e.sqrt) + if (math.abs(nsqrt - e.sqrt) < 0.01) { + results.remove(num) + } else e.sqrt = nsqrt + } + } + +Note that in the above Babylonian method square root computation +(\[[3][3]\]) some numbers may converge much faster than the others. For +this reason, we want to remove them from `results` so that only those +elements that need to be worked on are traversed. + +Another example is the breadth-first search algorithm, which iteratively expands the nodefront +until either it finds some path to the target or there are no more +nodes to expand. We define a node on a 2D map as a tuple of +`Int`s. We define the `map` as a 2D array of booleans which denote is +the respective slot occupied or not. We then declare 2 concurrent trie +maps-- `open` which contains all the nodes which have to be +expanded (the nodefront), and `closed` which contains all the nodes which have already +been expanded. We want to start the search from the corners of the map and +find a path to the center of the map-- we initialize the `open` map +with appropriate nodes. Then we iteratively expand all the nodes in +the `open` map in parallel until there are no more nodes to expand. +Each time a node is expanded, it is removed from the `open` map and +placed in the `closed` map. +Once done, we output the path from the target to the initial node. + + val length = 1000 + + // define the Node type + type Node = (Int, Int); + type Parent = (Int, Int); + + // operations on the Node type + def up(n: Node) = (n._1, n._2 - 1); + def down(n: Node) = (n._1, n._2 + 1); + def left(n: Node) = (n._1 - 1, n._2); + def right(n: Node) = (n._1 + 1, n._2); + + // create a map and a target + val target = (length / 2, length / 2); + val map = Array.tabulate(length, length)((x, y) => (x % 3) != 0 || (y % 3) != 0 || (x, y) == target) + def onMap(n: Node) = n._1 >= 0 && n._1 < length && n._2 >= 0 && n._2 < length + + // open list - the nodefront + // closed list - nodes already processed + val open = ParTrieMap[Node, Parent]() + val closed = ParTrieMap[Node, Parent]() + + // add a couple of starting positions + open((0, 0)) = null + open((length - 1, length - 1)) = null + open((0, length - 1)) = null + open((length - 1, 0)) = null + + // greedy bfs path search + while (open.nonEmpty && !open.contains(target)) { + for ((node, parent) <- open) { + def expand(next: Node) { + if (onMap(next) && map(next._1)(next._2) && !closed.contains(next) && !open.contains(next)) { + open(next) = node + } + } + expand(up(node)) + expand(down(node)) + expand(left(node)) + expand(right(node)) + closed(node) = parent + open.remove(node) + } + } + + // print path + var pathnode = open(target) + while (closed.contains(pathnode)) { + print(pathnode + "->") + pathnode = closed(pathnode) + } + println() + + +The concurrent tries also support a linearizable, lock-free, constant +time `snapshot` operation. This operation creates a new concurrent +trie with all the elements at a specific point in time, thus in effect +capturing the state of the trie at a specific point. +The `snapshot` operation merely creates +a new root for the concurrent trie. Subsequent updates lazily rebuild the part of +the concurrent trie relevant to the update and leave the rest of the concurrent trie +intact. First of all, this means that the snapshot operation by itself is not expensive +since it does not copy the elements. Second, since the copy-on-write optimization copies +only parts of the concurrent trie, subsequent modifications scale horizontally. +The `readOnlySnapshot` method is slightly more efficient than the +`snapshot` method, but returns a read-only map which cannot be +modified. Concurrent tries also support a linearizable, constant-time +`clear` operation based on the snapshot mechanism. +To learn more about how concurrent tries and snapshots work, see \[[1][1]\] and \[[2][2]\]. + +The iterators for concurrent tries are based on snapshots. Before the iterator +object gets created, a snapshot of the concurrent trie is taken, so the iterator +only traverses the elements in the trie at the time at which the snapshot was created. +Naturally, the iterators use the read-only snapshot. + +The `size` operation is also based on the snapshot. A straightforward implementation, the `size` +call would just create an iterator (i.e. a snapshot) and traverse the elements to count them. +Every call to `size` would thus require time linear in the number of elements. However, concurrent +tries have been optimized to cache sizes of their different parts, thus reducing the complexity +of the `size` method to amortized logarithmic time. In effect, this means that after calling +`size` once, subsequent calls to `size` will require a minimum amount of work, typically recomputing +the size only for those branches of the trie which have been modified since the last `size` call. +Additionally, size computation for parallel concurrent tries is performed in parallel. + + + +## References + +1. [Cache-Aware Lock-Free Concurrent Hash Tries][1] +2. [Concurrent Tries with Efficient Non-Blocking Snapshots][2] +3. [Methods of computing square roots][3] + + [1]: http://infoscience.epfl.ch/record/166908/files/ctries-techreport.pdf "Ctries-techreport" + [2]: http://lampwww.epfl.ch/~prokopec/ctries-snapshot.pdf "Ctries-snapshot" + [3]: http://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method "babylonian-method" diff --git a/es/overviews/parallel-collections/custom-parallel-collections.md b/es/overviews/parallel-collections/custom-parallel-collections.md new file mode 100644 index 0000000000..9f116b0229 --- /dev/null +++ b/es/overviews/parallel-collections/custom-parallel-collections.md @@ -0,0 +1,332 @@ +--- +layout: overview-large +title: Creating Custom Parallel Collections + +disqus: true + +partof: parallel-collections +num: 6 +languages: [ja] +--- + +## Parallel collections without combiners + +Just as it is possible to define custom sequential collections without +defining their builders, it is possible to define parallel collections without +defining their combiners. The consequence of not having a combiner is that +transformer methods (e.g. `map`, `flatMap`, `collect`, `filter`, ...) will by +default return a standard collection type which is nearest in the hierarchy. +For example, ranges do not have builders, so mapping elements of a range +creates a vector. + +In the following example we define a parallel string collection. Since strings +are logically immutable sequences, we have parallel strings inherit +`immutable.ParSeq[Char]`: + + class ParString(val str: String) + extends immutable.ParSeq[Char] { + +Next, we define methods found in every immutable sequence: + + def apply(i: Int) = str.charAt(i) + + def length = str.length + +We have to also define the sequential counterpart of this parallel collection. +In this case, we return the `WrappedString` class: + + def seq = new collection.immutable.WrappedString(str) + +Finally, we have to define a splitter for our parallel string collection. We +name the splitter `ParStringSplitter` and have it inherit a sequence splitter, +that is, `SeqSplitter[Char]`: + + def splitter = new ParStringSplitter(str, 0, str.length) + + class ParStringSplitter(private var s: String, private var i: Int, private val ntl: Int) + extends SeqSplitter[Char] { + + final def hasNext = i < ntl + + final def next = { + val r = s.charAt(i) + i += 1 + r + } + +Above, `ntl` represents the total length of the string, `i` is the current +position and `s` is the string itself. + +Parallel collection iterators or splitters require a few more methods in +addition to `next` and `hasNext` found in sequential collection iterators. +First of all, they have a method called `remaining` which returns the number +of elements this splitter has yet to traverse. Next, they have a method called +`dup` which duplicates the current splitter. + + def remaining = ntl - i + + def dup = new ParStringSplitter(s, i, ntl) + +Finally, methods `split` and `psplit` are used to create splitters which +traverse subsets of the elements of the current splitter. Method `split` has +the contract that it returns a sequence of splitters which traverse disjoint, +non-overlapping subsets of elements that the current splitter traverses, none +of which is empty. If the current splitter has 1 or less elements, then +`split` just returns a sequence of this splitter. Method `psplit` has to +return a sequence of splitters which traverse exactly as many elements as +specified by the `sizes` parameter. If the `sizes` parameter specifies less +elements than the current splitter, then an additional splitter with the rest +of the elements is appended at the end. If the `sizes` parameter requires more +elements than there are remaining in the current splitter, it will append an +empty splitter for each size. Finally, calling either `split` or `psplit` +invalidates the current splitter. + + def split = { + val rem = remaining + if (rem >= 2) psplit(rem / 2, rem - rem / 2) + else Seq(this) + } + + def psplit(sizes: Int*): Seq[ParStringSplitter] = { + val splitted = new ArrayBuffer[ParStringSplitter] + for (sz <- sizes) { + val next = (i + sz) min ntl + splitted += new ParStringSplitter(s, i, next) + i = next + } + if (remaining > 0) splitted += new ParStringSplitter(s, i, ntl) + splitted + } + } + } + +Above, `split` is implemented in terms of `psplit`, which is often the case +with parallel sequences. Implementing a splitter for parallel maps, sets or +iterables is often easier, since it does not require `psplit`. + +Thus, we obtain a parallel string class. The only downside is that calling transformer methods +such as `filter` will not produce a parallel string, but a parallel vector instead, which +may be suboptimal - producing a string again from the vector after filtering may be costly. + + +## Parallel collections with combiners + +Lets say we want to `filter` the characters of the parallel string, to get rid +of commas for example. As noted above, calling `filter` produces a parallel +vector and we want to obtain a parallel string (since some interface in the +API might require a sequential string). + +To avoid this, we have to write a combiner for the parallel string collection. +We will also inherit the `ParSeqLike` trait this time to ensure that return +type of `filter` is more specific - a `ParString` instead of a `ParSeq[Char]`. +The `ParSeqLike` has a third type parameter which specifies the type of the +sequential counterpart of the parallel collection (unlike sequential `*Like` +traits which have only two type parameters). + + class ParString(val str: String) + extends immutable.ParSeq[Char] + with ParSeqLike[Char, ParString, collection.immutable.WrappedString] + +All the methods remain the same as before, but we add an additional protected method `newCombiner` which +is internally used by `filter`. + + protected[this] override def newCombiner: Combiner[Char, ParString] = new ParStringCombiner + +Next we define the `ParStringCombiner` class. Combiners are subtypes of +builders and they introduce an additional method called `combine`, which takes +another combiner as an argument and returns a new combiner which contains the +elements of both the current and the argument combiner. The current and the +argument combiner are invalidated after calling `combine`. If the argument is +the same object as the current combiner, then `combine` just returns the +current combiner. This method is expected to be efficient, having logarithmic +running time with respect to the number of elements in the worst case, since +it is called multiple times during a parallel computation. + +Our `ParStringCombiner` will internally maintain a sequence of string +builders. It will implement `+=` by adding an element to the last string +builder in the sequence, and `combine` by concatenating the lists of string +builders of the current and the argument combiner. The `result` method, which +is called at the end of the parallel computation, will produce a parallel +string by appending all the string builders together. This way, elements are +copied only once at the end instead of being copied every time `combine` is +called. Ideally, we would like to parallelize this process and copy them in +parallel (this is being done for parallel arrays), but without tapping into +the internal represenation of strings this is the best we can do-- we have to +live with this sequential bottleneck. + + private class ParStringCombiner extends Combiner[Char, ParString] { + var sz = 0 + val chunks = new ArrayBuffer[StringBuilder] += new StringBuilder + var lastc = chunks.last + + def size: Int = sz + + def +=(elem: Char): this.type = { + lastc += elem + sz += 1 + this + } + + def clear = { + chunks.clear + chunks += new StringBuilder + lastc = chunks.last + sz = 0 + } + + def result: ParString = { + val rsb = new StringBuilder + for (sb <- chunks) rsb.append(sb) + new ParString(rsb.toString) + } + + def combine[U <: Char, NewTo >: ParString](other: Combiner[U, NewTo]) = if (other eq this) this else { + val that = other.asInstanceOf[ParStringCombiner] + sz += that.sz + chunks ++= that.chunks + lastc = chunks.last + this + } + } + + +## How do I implement my combiner in general? + +There are no predefined recipes-- it depends on the data-structure at +hand, and usually requires a bit of ingenuity on the implementer's +part. However there are a few approaches usually taken: + +1. Concatenation and merge. Some data-structures have efficient +implementations (usually logarithmic) of these operations. +If the collection at hand is backed by such a data-structure, +its combiner can be the collection itself. Finger trees, +ropes and various heaps are particularly suitable for such an approach. + +2. Two-phase evaluation. An approach taken in parallel arrays and +parallel hash tables, it assumes the elements can be efficiently +partially sorted into concatenable buckets from which the final +data-structure can be constructed in parallel. In the first phase +different procesors populate these buckets independently and +concatenate the buckets together. In the second phase, the data +structure is allocated and different processors populate different +parts of the datastructure in parallel using elements from disjoint +buckets. +Care must be taken that different processors never modify the same +part of the datastructure, otherwise subtle concurrency errors may occur. +This approach is easily applicable to random access sequences, as we +have shown in the previous section. + +3. A concurrent data-structure. While the last two approaches actually +do not require any synchronization primitives in the data-structure +itself, they assume that it can be constructed concurrently in a way +such that two different processors never modify the same memory +location. There exists a large number of concurrent data-structures +that can be modified safely by multiple processors-- concurrent skip lists, +concurrent hash tables, split-ordered lists, concurrent avl trees, to +name a few. +An important consideration in this case is that the concurrent +data-structure has a horizontally scalable insertion method. +For concurrent parallel collections the combiner can be the collection +itself, and a single combiner instance is shared between all the +processors performing a parallel operation. + + +## Integration with the collections framework + +Our `ParString` class is not complete yet. Although we have implemented a +custom combiner which will be used by methods such as `filter`, `partition`, +`takeWhile` or `span`, most transformer methods require an implicit +`CanBuildFrom` evidence (see Scala collections guide for a full explanation). +To make it available and completely integrate `ParString` with the collections +framework, we have to mix an additional trait called `GenericParTemplate` and +define the companion object of `ParString`. + + class ParString(val str: String) + extends immutable.ParSeq[Char] + with GenericParTemplate[Char, ParString] + with ParSeqLike[Char, ParString, collection.immutable.WrappedString] { + + def companion = ParString + +Inside the companion object we provide an implicit evidence for the `CanBuildFrom` parameter. + + object ParString { + implicit def canBuildFrom: CanCombineFrom[ParString, Char, ParString] = + new CanCombinerFrom[ParString, Char, ParString] { + def apply(from: ParString) = newCombiner + def apply() = newCombiner + } + + def newBuilder: Combiner[Char, ParString] = newCombiner + + def newCombiner: Combiner[Char, ParString] = new ParStringCombiner + + def apply(elems: Char*): ParString = { + val cb = newCombiner + cb ++= elems + cb.result + } + } + + + +## Further customizations-- concurrent and other collections + +Implementing a concurrent collection (unlike parallel collections, concurrent +collections are ones that can be concurrently modified, like +`collection.concurrent.TrieMap`) is not always straightforward. Combiners in +particular often require a lot of thought. In most _parallel_ collections +described so far, combiners use a two-step evaluation. In the first step the +elements are added to the combiners by different processors and the combiners +are merged together. In the second step, after all the elements are available, +the resulting collection is constructed. + +Another approach to combiners is to construct the resulting collection as the +elements. This requires the collection to be thread-safe-- a combiner must +allow _concurrent_ element insertion. In this case one combiner is shared by +all the processors. + +To parallelize a concurrent collection, its combiners must override the method +`canBeShared` to return `true`. This will ensure that only one combiner is +created when a parallel operation is invoked. Next, the `+=` method must be +thread-safe. Finally, method `combine` still returns the current combiner if +the current combiner and the argument combiner are the same, and is free to +throw an exception otherwise. + +Splitters are divided into smaller splitters to achieve better load balancing. +By default, information returned by the `remaining` method is used to decide +when to stop dividing the splitter. For some collections, calling the +`remaining` method may be costly and some other means should be used to decide +when to divide the splitter. In this case, one should override the +`shouldSplitFurther` method in the splitter. + +The default implementation divides the splitter if the number of remaining +elements is greater than the collection size divided by eight times the +parallelism level. + + def shouldSplitFurther[S](coll: ParIterable[S], parallelismLevel: Int) = + remaining > thresholdFromSize(coll.size, parallelismLevel) + +Equivalently, a splitter can hold a counter on how many times it was split and +implement `shouldSplitFurther` by returning `true` if the split count is +greater than `3 + log(parallelismLevel)`. This avoids having to call +`remaining`. + +Furthermore, if calling `remaining` is not a cheap operation for a particular +collection (i.e. it requires evaluating the number of elements in the +collection), then the method `isRemainingCheap` in splitters should be +overridden to return `false`. + +Finally, if the `remaining` method in splitters is extremely cumbersome to +implement, you can override the method `isStrictSplitterCollection` in its +collection to return `false`. Such collections will fail to execute some +methods which rely on splitters being strict, i.e. returning a correct value +in the `remaining` method. Importantly, this does not effect methods used in +for-comprehensions. + + + + + + + diff --git a/es/overviews/parallel-collections/es/overview.md b/es/overviews/parallel-collections/es/overview.md new file mode 100644 index 0000000000..d0f379d6d3 --- /dev/null +++ b/es/overviews/parallel-collections/es/overview.md @@ -0,0 +1,244 @@ +--- +layout: overview-large +title: Overview + +disqus: true + +partof: parallel-collections +num: 1 +languages: [ja] +--- + +**Autores originales: Aleksandar Prokopec, Heather Miller** + +**Traducción y arreglos: Santiago Basulto** + +## Motivación + +En el medio del cambio en los recientes años de los fabricantes de procesadores de arquitecturas simples a arquitecturas multi-nucleo, tanto el ámbito académico, como el industrial coinciden que la _Programación Paralela_ sigue siendo un gran desafío. + +Las Colecciones Paralelizadas (Parallel collections, en inglés) fueron incluidas en la librería del lenguaje Scala en un esfuerzo de facilitar la programación paralela al abstraer a los usuarios de detalles de paralelización de bajo nivel, mientras se provee con una abstracción de alto nivel, simple y familiar. La esperanza era, y sigue siendo, que el paralelismo implícito detrás de una abstracción de colecciones (como lo es el actual framework de colecciones del lenguaje) acercara la ejecución paralela confiable, un poco más al trabajo diario de los desarrolladores. + +La idea es simple: las colecciones son abstracciones de programación ficientemente entendidas y a su vez son frecuentemente usadas. Dada su regularidad, es posible que sean paralelizadas eficiente y transparentemente. Al permitirle al usuario intercambiar colecciones secuenciales por aquellas que son operadas en paralelo, las colecciones paralelizadas de Scala dan un gran paso hacia la posibilidad de que el paralelismo sea introducido cada vez más frecuentemente en nuestro código. + +Veamos el siguiente ejemplo secuencial, donde realizamos una operación monádica en una colección lo suficientemente grande. + + val list = (1 to 10000).toList + list.map(_ + 42) + +Para realizar la misma operación en paralelo, lo único que devemos incluir, es la invocación al método `par` en la colección secuencial `list`. Después de eso, es posible utilizar la misma colección paralelizada de la misma manera que normalmente la usariamos si fuera una colección secuencial. El ejemplo superior puede ser paralelizado al hacer simplemente lo siguiente: + + list.par.map(_ + 42) + +El diseño de la librería de colecciones paralelizadas de Scala está inspirada y fuertemente integrada con la librería estandar de colecciones (secuenciales) del lenguaje (introducida en la versión 2.8). Se provee te una contraparte paralelizada a un número importante de estructuras de datos de la librería de colecciones (secuenciales) de Scala, incluyendo: + +* `ParArray` +* `ParVector` +* `mutable.ParHashMap` +* `mutable.ParHashSet` +* `immutable.ParHashMap` +* `immutable.ParHashSet` +* `ParRange` +* `ParTrieMap` (`collection.concurrent.TrieMap`s are new in 2.10) + +Además de una arquitectura común, la librería de colecciones paralelizadas de Scala también comparte la _extensibilidad_ con la librería de colecciones secuenciales. Es decir, de la misma manera que los usuarios pueden integrar sus propios tipos de tipos de colecciones de la librería normal de colecciones secuenciales, pueden realizarlo con la librería de colecciones paralelizadas, heredando automáticamente todas las operaciones paralelas disponibles en las demás colecciones paralelizadas de la librería estandar. + +## Algunos Ejemplos + +To attempt to illustrate the generality and utility of parallel collections, +we provide a handful of simple example usages, all of which are transparently +executed in parallel. + +De forma de ilustrar la generalidad y utilidad de las colecciones paralelizadas, proveemos un conjunto de ejemplos de uso útiles, todos ellos siendo ejecutados en paralelo de forma totalmente transparente al usuario. + +_Nota:_ Algunos de los siguientes ejemplos operan en colecciones pequeñas, lo cual no es recomendado. Son provistos como ejemplo para ilustrar solamente el propósito. Como una regla heurística general, los incrementos en velocidad de ejecución comienzan a ser notados cuando el tamaño de la colección es lo suficientemente grande, tipicamente algunos cuantos miles de elementos. (Para más información en la relación entre tamaño de una coleccion paralelizada y su performance, por favor véase [appropriate subsection]({{ site.baseurl}}/overviews/parallel-collections/performance.html#how_big_should_a_collection_be_to_go_parallel) en la sección [performance]({{ site.baseurl }}/overviews/parallel-collections/performance.html) (en inglés). + +#### map + +Usando un `map` paralelizado para transformar una colección de elementos tipo `String` a todos caracteres en mayúscula: + + scala> val apellidos = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par + apellidos: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) + + scala> apellidos.map(_.toUpperCase) + res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(SMITH, JONES, FRANKENSTEIN, BACH, JACKSON, RODIN) + +#### fold + +Sumatoria mediante `fold` en un `ParArray`: + + scala> val parArray = (1 to 1000000).toArray.par + parArray: scala.collection.parallel.mutable.ParArray[Int] = ParArray(1, 2, 3, ... + + scala> parArray.fold(0)(_ + _) + res0: Int = 1784293664 + +#### filtrando + + +Usando un filtrado mediante `filter` paralelizado para seleccionar los apellidos que alfabéticamente preceden la letra "K": + + scala> val apellidos = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par + apellidos: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) + + scala> apellidos.filter(_.head >= 'J') + res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Jackson, Rodin) + +## Creación de colecciones paralelizadas + +Las colecciones paralelizadas están pensadas para ser usadas exactamente de la misma manera que las colecciones secuenciales --la única diferencia notoria es cómo _obtener_ una colección paralelizada. + +Generalmente se tienen dos opciones para la creación de colecciones paralelizadas: + +Primero al utilizar la palabra clave `new` y una sentencia de importación apropiada: + + import scala.collection.parallel.immutable.ParVector + val pv = new ParVector[Int] + +Segundo, al _convertir_ desde una colección secuencial: + + val pv = Vector(1,2,3,4,5,6,7,8,9).par + +What's important to expand upon here are these conversion methods-- sequential +collections can be converted to parallel collections by invoking the +sequential collection's `par` method, and likewise, parallel collections can +be converted to sequential collections by invoking the parallel collection's +`seq` method. + +_Of Note:_ Collections that are inherently sequential (in the sense that the +elements must be accessed one after the other), like lists, queues, and +streams, are converted to their parallel counterparts by copying the elements +into a similar parallel collection. An example is `List`-- it's converted into +a standard immutable parallel sequence, which is a `ParVector`. Of course, the +copying required for these collection types introduces an overhead not +incurred by any other collection types, like `Array`, `Vector`, `HashMap`, etc. + +For more information on conversions on parallel collections, see the +[conversions]({{ site.baseurl }}/overviews/parallel-collections/converesions.html) +and [concrete parallel collection classes]({{ site.baseurl }}/overviews/parallel-collections/concrete-parallel-collections.html) +sections of thise guide. + +## Semantics + +While the parallel collections abstraction feels very much the same as normal +sequential collections, it's important to note that its semantics differs, +especially with regards to side-effects and non-associative operations. + +In order to see how this is the case, first, we visualize _how_ operations are +performed in parallel. Conceptually, Scala's parallel collections framework +parallelizes an operation on a parallel collection by recursively "splitting" +a given collection, applying an operation on each partition of the collection +in parallel, and re-"combining" all of the results that were completed in +parallel. + +These concurrent, and "out-of-order" semantics of parallel collections lead to +the following two implications: + +1. **Side-effecting operations can lead to non-determinism** +2. **Non-associative operations lead to non-determinism** + +### Side-Effecting Operations + +Given the _concurrent_ execution semantics of the parallel collections +framework, operations performed on a collection which cause side-effects +should generally be avoided, in order to maintain determinism. A simple +example is by using an accessor method, like `foreach` to increment a `var` +declared outside of the closure which is passed to `foreach`. + + scala> var sum = 0 + sum: Int = 0 + + scala> val list = (1 to 1000).toList.par + list: scala.collection.parallel.immutable.ParSeq[Int] = ParVector(1, 2, 3,… + + scala> list.foreach(sum += _); sum + res01: Int = 467766 + + scala> var sum = 0 + sum: Int = 0 + + scala> list.foreach(sum += _); sum + res02: Int = 457073 + + scala> var sum = 0 + sum: Int = 0 + + scala> list.foreach(sum += _); sum + res03: Int = 468520 + +Here, we can see that each time `sum` is reinitialized to 0, and `foreach` is +called again on `list`, `sum` holds a different value. The source of this +non-determinism is a _data race_-- concurrent reads/writes to the same mutable +variable. + +In the above example, it's possible for two threads to read the _same_ value +in `sum`, to spend some time doing some operation on that value of `sum`, and +then to attempt to write a new value to `sum`, potentially resulting in an +overwrite (and thus, loss) of a valuable result, as illustrated below: + + ThreadA: read value in sum, sum = 0 value in sum: 0 + ThreadB: read value in sum, sum = 0 value in sum: 0 + ThreadA: increment sum by 760, write sum = 760 value in sum: 760 + ThreadB: increment sum by 12, write sum = 12 value in sum: 12 + +The above example illustrates a scenario where two threads read the same +value, `0`, before one or the other can sum `0` with an element from their +partition of the parallel collection. In this case, `ThreadA` reads `0` and +sums it with its element, `0+760`, and in the case of `ThreadB`, sums `0` with +its element, `0+12`. After computing their respective sums, they each write +their computed value in `sum`. Since `ThreadA` beats `ThreadB`, it writes +first, only for the value in `sum` to be overwritten shortly after by +`ThreadB`, in effect completely overwriting (and thus losing) the value `760`. + +### Non-Associative Operations + +Given this _"out-of-order"_ semantics, also must be careful to perform only +associative operations in order to avoid non-determinism. That is, given a +parallel collection, `pcoll`, one should be sure that when invoking a +higher-order function on `pcoll`, such as `pcoll.reduce(func)`, the order in +which `func` is applied to the elements of `pcoll` can be arbitrary. A simple, +but obvious example is a non-associative operation such as subtraction: + + scala> val list = (1 to 1000).toList.par + list: scala.collection.parallel.immutable.ParSeq[Int] = ParVector(1, 2, 3,… + + scala> list.reduce(_-_) + res01: Int = -228888 + + scala> list.reduce(_-_) + res02: Int = -61000 + + scala> list.reduce(_-_) + res03: Int = -331818 + +In the above example, we take a `ParVector[Int]`, invoke `reduce`, and pass to +it `_-_`, which simply takes two unnamed elements, and subtracts the first +from the second. Due to the fact that the parallel collections framework spawns +threads which, in effect, independently perform `reduce(_-_)` on different +sections of the collection, the result of two runs of `reduce(_-_)` on the +same collection will not be the same. + +_Note:_ Often, it is thought that, like non-associative operations, non-commutative +operations passed to a higher-order function on a parallel +collection likewise result in non-deterministic behavior. This is not the +case, a simple example is string concatenation-- an associative, but non- +commutative operation: + + scala> val strings = List("abc","def","ghi","jk","lmnop","qrs","tuv","wx","yz").par + strings: scala.collection.parallel.immutable.ParSeq[java.lang.String] = ParVector(abc, def, ghi, jk, lmnop, qrs, tuv, wx, yz) + + scala> val alphabet = strings.reduce(_++_) + alphabet: java.lang.String = abcdefghijklmnopqrstuvwxyz + +The _"out of order"_ semantics of parallel collections only means that +the operation will be executed out of order (in a _temporal_ sense. That is, +non-sequentially), it does not mean that the result will be +re-"*combined*" out of order (in a _spatial_ sense). On the contrary, results +will generally always be reassembled _in order_-- that is, a parallel collection +broken into partitions A, B, C, in that order, will be reassembled once again +in the order A, B, C. Not some other arbitrary order like B, C, A. + +For more on how parallel collections split and combine operations on different +parallel collection types, see the [Architecture]({{ site.baseurl }}/overviews +/parallel-collections/architecture.html) section of this guide. + diff --git a/es/overviews/parallel-collections/overview.md b/es/overviews/parallel-collections/overview.md new file mode 100644 index 0000000000..1bcb67d437 --- /dev/null +++ b/es/overviews/parallel-collections/overview.md @@ -0,0 +1,219 @@ +--- +layout: overview-large +title: Overview + +disqus: true + +partof: parallel-collections +num: 1 +languages: [ja] +--- + +**Autores originales: Aleksandar Prokopec, Heather Miller** + +**Traducción y arreglos: Santiago Basulto** + +## Motivación + +En el medio del cambio en los recientes años de los fabricantes de procesadores de arquitecturas simples a arquitecturas multi-nucleo, tanto el ámbito académico, como el industrial coinciden que la _Programación Paralela_ sigue siendo un gran desafío. + +Las Colecciones Paralelizadas (Parallel collections, en inglés) fueron incluidas en la librería del lenguaje Scala en un esfuerzo de facilitar la programación paralela al abstraer a los usuarios de detalles de paralelización de bajo nivel, mientras se provee con una abstracción de alto nivel, simple y familiar. La esperanza era, y sigue siendo, que el paralelismo implícito detrás de una abstracción de colecciones (como lo es el actual framework de colecciones del lenguaje) acercara la ejecución paralela confiable, un poco más al trabajo diario de los desarrolladores. + +La idea es simple: las colecciones son abstracciones de programación ficientemente entendidas y a su vez son frecuentemente usadas. Dada su regularidad, es posible que sean paralelizadas eficiente y transparentemente. Al permitirle al usuario intercambiar colecciones secuenciales por aquellas que son operadas en paralelo, las colecciones paralelizadas de Scala dan un gran paso hacia la posibilidad de que el paralelismo sea introducido cada vez más frecuentemente en nuestro código. + +Veamos el siguiente ejemplo secuencial, donde realizamos una operación monádica en una colección lo suficientemente grande. + + val list = (1 to 10000).toList + list.map(_ + 42) + +Para realizar la misma operación en paralelo, lo único que devemos incluir, es la invocación al método `par` en la colección secuencial `list`. Después de eso, es posible utilizar la misma colección paralelizada de la misma manera que normalmente la usariamos si fuera una colección secuencial. El ejemplo superior puede ser paralelizado al hacer simplemente lo siguiente: + + list.par.map(_ + 42) + +El diseño de la librería de colecciones paralelizadas de Scala está inspirada y fuertemente integrada con la librería estandar de colecciones (secuenciales) del lenguaje (introducida en la versión 2.8). Se provee te una contraparte paralelizada a un número importante de estructuras de datos de la librería de colecciones (secuenciales) de Scala, incluyendo: + +* `ParArray` +* `ParVector` +* `mutable.ParHashMap` +* `mutable.ParHashSet` +* `immutable.ParHashMap` +* `immutable.ParHashSet` +* `ParRange` +* `ParTrieMap` (`collection.concurrent.TrieMap`s are new in 2.10) + +Además de una arquitectura común, la librería de colecciones paralelizadas de Scala también comparte la _extensibilidad_ con la librería de colecciones secuenciales. Es decir, de la misma manera que los usuarios pueden integrar sus propios tipos de tipos de colecciones de la librería normal de colecciones secuenciales, pueden realizarlo con la librería de colecciones paralelizadas, heredando automáticamente todas las operaciones paralelas disponibles en las demás colecciones paralelizadas de la librería estandar. + +## Algunos Ejemplos + +To attempt to illustrate the generality and utility of parallel collections, +we provide a handful of simple example usages, all of which are transparently +executed in parallel. + +De forma de ilustrar la generalidad y utilidad de las colecciones paralelizadas, proveemos un conjunto de ejemplos de uso útiles, todos ellos siendo ejecutados en paralelo de forma totalmente transparente al usuario. + +_Nota:_ Algunos de los siguientes ejemplos operan en colecciones pequeñas, lo cual no es recomendado. Son provistos como ejemplo para ilustrar solamente el propósito. Como una regla heurística general, los incrementos en velocidad de ejecución comienzan a ser notados cuando el tamaño de la colección es lo suficientemente grande, tipicamente algunos cuantos miles de elementos. (Para más información en la relación entre tamaño de una coleccion paralelizada y su performance, por favor véase [appropriate subsection]({{ site.baseurl}}/es/overviews/parallel-collections/performance.html#how_big_should_a_collection_be_to_go_parallel) en la sección [performance]({{ site.baseurl }}/es/overviews/parallel-collections/performance.html) (en inglés). + +#### map + +Usando un `map` paralelizado para transformar una colección de elementos tipo `String` a todos caracteres en mayúscula: + + scala> val apellidos = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par + apellidos: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) + + scala> apellidos.map(_.toUpperCase) + res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(SMITH, JONES, FRANKENSTEIN, BACH, JACKSON, RODIN) + +#### fold + +Sumatoria mediante `fold` en un `ParArray`: + + scala> val parArray = (1 to 1000000).toArray.par + parArray: scala.collection.parallel.mutable.ParArray[Int] = ParArray(1, 2, 3, ... + + scala> parArray.fold(0)(_ + _) + res0: Int = 1784293664 + +#### filtrando + + +Usando un filtrado mediante `filter` paralelizado para seleccionar los apellidos que alfabéticamente preceden la letra "K": + + scala> val apellidos = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par + apellidos: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) + + scala> apellidos.filter(_.head >= 'J') + res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Jackson, Rodin) + +## Creación de colecciones paralelizadas + +Las colecciones paralelizadas están pensadas para ser usadas exactamente de la misma manera que las colecciones secuenciales --la única diferencia notoria es cómo _obtener_ una colección paralelizada. + +Generalmente se tienen dos opciones para la creación de colecciones paralelizadas: + +Primero al utilizar la palabra clave `new` y una sentencia de importación apropiada: + + import scala.collection.parallel.immutable.ParVector + val pv = new ParVector[Int] + +Segundo, al _convertir_ desde una colección secuencial: + + val pv = Vector(1,2,3,4,5,6,7,8,9).par + +Lo que es importante desarrollar aquí son estos métodos para la conversión de colecciones. Las colecciones secuenciales pueden ser convertiadas a colecciones paralelizadas mediante la invocación del método `par`, y de la misma manera, las colecciones paralelizadas pueden ser convertidas a colecciones secuenciales mediante el método `seq`. + +_Nota:_ Las colecciones que son inherentemente secuenciales (en el sentido que sus elementos deben ser accedidos uno a uno), como las listas, colas y streams (a veces llamados flujos), son convertidos a sus contrapartes paralelizadas al copiar los todos sus elementos. Un ejemplo es la clase `List` --es convertida a una secuencia paralelizada inmutable común, que es un `ParVector`. Por supuesto, el tener que copiar los elementos para estas colecciones involucran una carga más de trabajo que no se sufre con otros tipos como: `Array`, `Vector`, `HashMap`, etc. + +For more information on conversions on parallel collections, see the +[conversions]({{ site.baseurl }}/overviews/parallel-collections/converesions.html) +and [concrete parallel collection classes]({{ site.baseurl }}/overviews/parallel-collections/concrete-parallel-collections.html) +sections of thise guide. + +Para más información sobre la conversión de colecciones paralelizadas, véase los artículos sobre [conversiones]({{ site.baseurl }}/es/overviews/parallel-collections/converesions.html) y [clases concretas de colecciones paralelizadas]({{ site.baseurl }}/es/overviews/parallel-collections/concrete-parallel-collections.html) de esta misma serie. + +## Entendiendo las colecciones paralelizadas + +A pesar de que las abstracciones de las colecciones paralelizadas se parecen mucho a las colecciones secuenciales normales, es importante notar que su semántica difiere, especialmente con relación a efectos secundarios (o colaterales, según algunas traducciones) y operaciones no asociativas. + +Para entender un poco más esto, primero analizaremos _cómo_ son realizadas las operaciones paralelas. Conceptualmente, el framework de colecciones paralelizadas de Scala paraleliza una operación al "dividir" recursivamente una colección dada, aplicando una operación en cada partición de la colección en paralelo y recombinando todos los resultados que fueron completados en paralelo. + +Esta ejecución concurrente y fuera de orden de las colecciones paralelizadas llevan a dos implicancias que es importante notar: + +1. **Las operaciones con efectos secundarios pueden llegar a resultados no deterministas** +2. **Operaciones no asociativas generan resultados no deterministas** + +### Operaciones con efectos secundarios + +Given the _concurrent_ execution semantics of the parallel collections +framework, operations performed on a collection which cause side-effects +should generally be avoided, in order to maintain determinism. A simple +example is by using an accessor method, like `foreach` to increment a `var` +declared outside of the closure which is passed to `foreach`. + +Dada la ejecución _concurrente_ del framework de colecciones paralelizadas, las operaciones que generen efectos secundarios generalmente deben ser evitadas, de manera de mantener el "determinismo". + +Veamos un ejemplo: + + scala> var sum = 0 + sum: Int = 0 + + scala> val list = (1 to 1000).toList.par + list: scala.collection.parallel.immutable.ParSeq[Int] = ParVector(1, 2, 3,… + + scala> list.foreach(sum += _); sum + res01: Int = 467766 + + scala> var sum = 0 + sum: Int = 0 + + scala> list.foreach(sum += _); sum + res02: Int = 457073 + + scala> var sum = 0 + sum: Int = 0 + + scala> list.foreach(sum += _); sum + res03: Int = 468520 + +Acá podemos ver que cada vez que `sum` es reinicializado a 0, e invocamos el método `foreach` en nuestro objeto `list`, el valor de `sum` resulta ser distinto. La razón de este no-determinismo es una _condición de carrera_ -- lecturas/escrituras concurrentes a la misma variable mutable. + +En el ejemplo anterior, es posible para dos hilos leer el _mismo_ valor de `sum`, demorarse un tiempo realizando la operación que tienen que hacer sobre `sum`, y después volver a escribir ese nuevo valor a `sum`, lo que probablemente resulte en una sobreescritura (y por lo tanto pérdida) de un valor anterior que generó otro hilo. Veamos otro ejemplo: + + HiloA: lee el valor en sum, sum = 0 valor de sum: 0 + HiloB: lee el valor en sum, sum = 0 valor de sum: 0 + HiloA: incrementa el valor de sum a 760, graba sum = 760 valor de sum: 760 + HiloA: incrementa el valor de sum a 12, graba sum = 12 valor de sum: 12 + +Este ejemplo ilustra un escenario donde dos hilos leen el mismo valor, `0`, antes que el otro pueda sumar su parte de la ejecución sobre la colección paralela. En este caso el `HiloA` lee `0` y le suma el valor de su cómputo, `0+760`, y en el caso del `HiloB`, le suma al valor leido `0` su resultado, quedando `0+12`. Después de computar sus respectivas sumas, ambos escriben el valor en `sum`. Ya que el `HiloA` llega a escribir antes que el `HiloB` (por nada en particular, solamente coincidencia que en este caso llegue primero el `HiloA`), su valor se pierde, porque seguidamente llega a escribir el `HiloB` y borra el valor previamente guardado. Esto se llama _condición de carrera_ porque el valor termina resultando una cuestión de suerte, o aleatoria, de quién llega antes o después a escribir el valor final. + +### Non-Associative Operations + +Given this _"out-of-order"_ semantics, also must be careful to perform only +associative operations in order to avoid non-determinism. That is, given a +parallel collection, `pcoll`, one should be sure that when invoking a +higher-order function on `pcoll`, such as `pcoll.reduce(func)`, the order in +which `func` is applied to the elements of `pcoll` can be arbitrary. A simple, +but obvious example is a non-associative operation such as subtraction: + + scala> val list = (1 to 1000).toList.par + list: scala.collection.parallel.immutable.ParSeq[Int] = ParVector(1, 2, 3,… + + scala> list.reduce(_-_) + res01: Int = -228888 + + scala> list.reduce(_-_) + res02: Int = -61000 + + scala> list.reduce(_-_) + res03: Int = -331818 + +In the above example, we take a `ParVector[Int]`, invoke `reduce`, and pass to +it `_-_`, which simply takes two unnamed elements, and subtracts the first +from the second. Due to the fact that the parallel collections framework spawns +threads which, in effect, independently perform `reduce(_-_)` on different +sections of the collection, the result of two runs of `reduce(_-_)` on the +same collection will not be the same. + +_Note:_ Often, it is thought that, like non-associative operations, non-commutative +operations passed to a higher-order function on a parallel +collection likewise result in non-deterministic behavior. This is not the +case, a simple example is string concatenation-- an associative, but non- +commutative operation: + + scala> val strings = List("abc","def","ghi","jk","lmnop","qrs","tuv","wx","yz").par + strings: scala.collection.parallel.immutable.ParSeq[java.lang.String] = ParVector(abc, def, ghi, jk, lmnop, qrs, tuv, wx, yz) + + scala> val alphabet = strings.reduce(_++_) + alphabet: java.lang.String = abcdefghijklmnopqrstuvwxyz + +The _"out of order"_ semantics of parallel collections only means that +the operation will be executed out of order (in a _temporal_ sense. That is, +non-sequentially), it does not mean that the result will be +re-"*combined*" out of order (in a _spatial_ sense). On the contrary, results +will generally always be reassembled _in order_-- that is, a parallel collection +broken into partitions A, B, C, in that order, will be reassembled once again +in the order A, B, C. Not some other arbitrary order like B, C, A. + +For more on how parallel collections split and combine operations on different +parallel collection types, see the [Architecture]({{ site.baseurl }}/overviews +/parallel-collections/architecture.html) section of this guide. + diff --git a/es/overviews/parallel-collections/performance.md b/es/overviews/parallel-collections/performance.md new file mode 100644 index 0000000000..bbece7aa8c --- /dev/null +++ b/es/overviews/parallel-collections/performance.md @@ -0,0 +1,284 @@ +--- +layout: overview-large +title: Measuring Performance + +disqus: true + +partof: parallel-collections +num: 8 +outof: 8 +languages: [ja] +--- + +## Performance on the JVM + +The performance model on the JVM is sometimes convoluted in commentaries about +it, and as a result is not well understood. For various reasons, some code may +not be as performant or as scalable as expected. Here, we provide a few +examples. + +One of the reasons is that the compilation process for a JVM application is +not the same as that of a statically compiled language (see \[[2][2]\]). The +Java and Scala compilers convert source code into JVM bytecode and do very +little optimization. On most modern JVMs, once the program bytecode is run, it +is converted into machine code for the computer architecture on which it is +being run. This is called the just-in-time compilation. The level of code +optimization is, however, low with just-in-time compilation, since it has to +be fast. To avoid recompiling, the so called HotSpot compiler only optimizes +parts of the code which are executed frequently. What this means for the +benchmark writer is that a program might have different performance each time +it is run. Executing the same piece of code (e.g. a method) multiple times in +the same JVM instance might give very different performance results depending +on whether the particular code was optimized in between the runs. +Additionally, measuring the execution time of some piece of code may include +the time during which the JIT compiler itself was performing the optimization, +thus giving inconsistent results. + +Another hidden execution that takes part on the JVM is the automatic memory +management. Every once in a while, the execution of the program is stopped and +a garbage collector is run. If the program being benchmarked allocates any +heap memory at all (and most JVM programs do), the garbage collector will have +to run, thus possibly distorting the measurement. To amortize the garbage +collection effects, the measured program should run many times to trigger many +garbage collections. + +One common cause of a performance deterioration is also boxing and unboxing +that happens implicitly when passing a primitive type as an argument to a +generic method. At runtime, primitive types are converted to objects which +represent them, so that they could be passed to a method with a generic type +parameter. This induces extra allocations and is slower, also producing +additional garbage on the heap. + +Where parallel performance is concerned, one common issue is memory +contention, as the programmer does not have explicit control about where the +objects are allocated. +In fact, due to GC effects, contention can occur at a later stage in +the application lifetime after objects get moved around in memory. +Such effects need to be taken into consideration when writing a benchmark. + + +## Microbenchmarking example + +There are several approaches to avoid the above effects during measurement. +First of all, the target microbenchmark must be executed enough times to make +sure that the just-in-time compiler compiled it to machine code and that it +was optimized. This is known as the warm-up phase. + +The microbenchmark itself should be run in a separate JVM instance to reduce +noise coming from garbage collection of the objects allocated by different +parts of the program or unrelated just-in-time compilation. + +It should be run using the server version of the HotSpot JVM, which does more +aggressive optimizations. + +Finally, to reduce the chance of a garbage collection occurring in the middle +of the benchmark, ideally a garbage collection cycle should occur prior to the +run of the benchmark, postponing the next cycle as far as possible. + +The `scala.testing.Benchmark` trait is predefined in the Scala standard +library and is designed with above in mind. Here is an example of benchmarking +a map operation on a concurrent trie: + + import collection.parallel.mutable.ParTrieMap + import collection.parallel.ForkJoinTaskSupport + + object Map extends testing.Benchmark { + val length = sys.props("length").toInt + val par = sys.props("par").toInt + val partrie = ParTrieMap((0 until length) zip (0 until length): _*) + + partrie.tasksupport = new ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(par)) + + def run = { + partrie map { + kv => kv + } + } + } + +The `run` method embodies the microbenchmark code which will be run +repetitively and whose running time will be measured. The object `Map` above +extends the `scala.testing.Benchmark` trait and parses system specified +parameters `par` for the parallelism level and `length` for the number of +elements in the trie. + +After compiling the program above, run it like this: + + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=300000 Map 10 + +The `server` flag specifies that the server VM should be used. The `cp` +specifies the classpath and includes classfiles in the current directory and +the scala library jar. Arguments `-Dpar` and `-Dlength` are the parallelism +level and the number of elements. Finally, `10` means that the benchmark +should be run that many times within the same JVM. + +Running times obtained by setting the `par` to `1`, `2`, `4` and `8` on a +quad-core i7 with hyperthreading: + + Map$ 126 57 56 57 54 54 54 53 53 53 + Map$ 90 99 28 28 26 26 26 26 26 26 + Map$ 201 17 17 16 15 15 16 14 18 15 + Map$ 182 12 13 17 16 14 14 12 12 12 + +We can see above that the running time is higher during the initial runs, but +is reduced after the code gets optimized. Further, we can see that the benefit +of hyperthreading is not high in this example, as going from `4` to `8` +threads results only in a minor performance improvement. + + +## How big should a collection be to go parallel? + +This is a question commonly asked. The answer is somewhat involved. + +The size of the collection at which the parallelization pays of really +depends on many factors. Some of them, but not all, include: + +- Machine architecture. Different CPU types have different + performance and scalability characteristics. Orthogonal to that, + whether the machine is multicore or has multiple processors + communicating via motherboard. +- JVM vendor and version. Different VMs apply different + optimizations to the code at runtime. They implement different memory + management and synchronization techniques. Some do not support + `ForkJoinPool`, reverting to `ThreadPoolExecutor`s, resulting in + more overhead. +- Per-element workload. A function or a predicate for a parallel + operation determines how big is the per-element workload. The + smaller the workload, the higher the number of elements needed to + gain speedups when running in parallel. +- Specific collection. For example, `ParArray` and + `ParTrieMap` have splitters that traverse the collection at + different speeds, meaning there is more per-element work in just the + traversal itself. +- Specific operation. For example, `ParVector` is a lot slower for + transformer methods (like `filter`) than it is for accessor methods (like `foreach`) +- Side-effects. When modifying memory areas concurrently or using + synchronization within the body of `foreach`, `map`, etc., + contention can occur. +- Memory management. When allocating a lot of objects a garbage + collection cycle can be triggered. Depending on how the references + to new objects are passed around, the GC cycle can take more or less time. + +Even in separation, it is not easy to reason about things above and +give a precise answer to what the collection size should be. To +roughly illustrate what the size should be, we give an example of +a cheap side-effect-free parallel vector reduce (in this case, sum) +operation performance on an i7 quad-core processor (not using +hyperthreading) on JDK7: + + import collection.parallel.immutable.ParVector + + object Reduce extends testing.Benchmark { + val length = sys.props("length").toInt + val par = sys.props("par").toInt + val parvector = ParVector((0 until length): _*) + + parvector.tasksupport = new collection.parallel.ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(par)) + + def run = { + parvector reduce { + (a, b) => a + b + } + } + } + + object ReduceSeq extends testing.Benchmark { + val length = sys.props("length").toInt + val vector = collection.immutable.Vector((0 until length): _*) + + def run = { + vector reduce { + (a, b) => a + b + } + } + } + +We first run the benchmark with `250000` elements and obtain the +following results, for `1`, `2` and `4` threads: + + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=250000 Reduce 10 10 + Reduce$ 54 24 18 18 18 19 19 18 19 19 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=2 -Dlength=250000 Reduce 10 10 + Reduce$ 60 19 17 13 13 13 13 14 12 13 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=4 -Dlength=250000 Reduce 10 10 + Reduce$ 62 17 15 14 13 11 11 11 11 9 + +We then decrease the number of elements down to `120000` and use `4` +threads to compare the time to that of a sequential vector reduce: + + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=4 -Dlength=120000 Reduce 10 10 + Reduce$ 54 10 8 8 8 7 8 7 6 5 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dlength=120000 ReduceSeq 10 10 + ReduceSeq$ 31 7 8 8 7 7 7 8 7 8 + +`120000` elements seems to be the around the threshold in this case. + +As another example, we take the `mutable.ParHashMap` and the `map` +method (a transformer method) and run the following benchmark in the same environment: + + import collection.parallel.mutable.ParHashMap + + object Map extends testing.Benchmark { + val length = sys.props("length").toInt + val par = sys.props("par").toInt + val phm = ParHashMap((0 until length) zip (0 until length): _*) + + phm.tasksupport = new collection.parallel.ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(par)) + + def run = { + phm map { + kv => kv + } + } + } + + object MapSeq extends testing.Benchmark { + val length = sys.props("length").toInt + val hm = collection.mutable.HashMap((0 until length) zip (0 until length): _*) + + def run = { + hm map { + kv => kv + } + } + } + +For `120000` elements we get the following times when ranging the +number of threads from `1` to `4`: + + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=120000 Map 10 10 + Map$ 187 108 97 96 96 95 95 95 96 95 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=2 -Dlength=120000 Map 10 10 + Map$ 138 68 57 56 57 56 56 55 54 55 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=4 -Dlength=120000 Map 10 10 + Map$ 124 54 42 40 38 41 40 40 39 39 + +Now, if we reduce the number of elements to `15000` and compare that +to the sequential hashmap: + + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=15000 Map 10 10 + Map$ 41 13 10 10 10 9 9 9 10 9 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=2 -Dlength=15000 Map 10 10 + Map$ 48 15 9 8 7 7 6 7 8 6 + java -server -cp .:../../build/pack/lib/scala-library.jar -Dlength=15000 MapSeq 10 10 + MapSeq$ 39 9 9 9 8 9 9 9 9 9 + +For this collection and this operation it makes sense +to go parallel when there are above `15000` elements (in general, +it is feasible to parallelize hashmaps and hashsets with fewer +elements than would be required for arrays or vectors). + + + + + +## References + +1. [Anatomy of a flawed microbenchmark, Brian Goetz][1] +2. [Dynamic compilation and performance measurement, Brian Goetz][2] + + [1]: http://www.ibm.com/developerworks/java/library/j-jtp02225/index.html "flawed-benchmark" + [2]: http://www.ibm.com/developerworks/library/j-jtp12214/ "dynamic-compilation" + + + From 7723bfe4a020efc2aac67164ba703f2c58b5ea8d Mon Sep 17 00:00:00 2001 From: Santiago Basulto Date: Sat, 7 Apr 2012 17:09:49 -0300 Subject: [PATCH 3/3] Final corrections --- .../parallel-collections/es/overview.md | 244 ------------------ es/overviews/parallel-collections/overview.md | 44 +--- overviews/parallel-collections/es/overview.md | 244 ------------------ 3 files changed, 11 insertions(+), 521 deletions(-) delete mode 100644 es/overviews/parallel-collections/es/overview.md delete mode 100644 overviews/parallel-collections/es/overview.md diff --git a/es/overviews/parallel-collections/es/overview.md b/es/overviews/parallel-collections/es/overview.md deleted file mode 100644 index d0f379d6d3..0000000000 --- a/es/overviews/parallel-collections/es/overview.md +++ /dev/null @@ -1,244 +0,0 @@ ---- -layout: overview-large -title: Overview - -disqus: true - -partof: parallel-collections -num: 1 -languages: [ja] ---- - -**Autores originales: Aleksandar Prokopec, Heather Miller** - -**Traducción y arreglos: Santiago Basulto** - -## Motivación - -En el medio del cambio en los recientes años de los fabricantes de procesadores de arquitecturas simples a arquitecturas multi-nucleo, tanto el ámbito académico, como el industrial coinciden que la _Programación Paralela_ sigue siendo un gran desafío. - -Las Colecciones Paralelizadas (Parallel collections, en inglés) fueron incluidas en la librería del lenguaje Scala en un esfuerzo de facilitar la programación paralela al abstraer a los usuarios de detalles de paralelización de bajo nivel, mientras se provee con una abstracción de alto nivel, simple y familiar. La esperanza era, y sigue siendo, que el paralelismo implícito detrás de una abstracción de colecciones (como lo es el actual framework de colecciones del lenguaje) acercara la ejecución paralela confiable, un poco más al trabajo diario de los desarrolladores. - -La idea es simple: las colecciones son abstracciones de programación ficientemente entendidas y a su vez son frecuentemente usadas. Dada su regularidad, es posible que sean paralelizadas eficiente y transparentemente. Al permitirle al usuario intercambiar colecciones secuenciales por aquellas que son operadas en paralelo, las colecciones paralelizadas de Scala dan un gran paso hacia la posibilidad de que el paralelismo sea introducido cada vez más frecuentemente en nuestro código. - -Veamos el siguiente ejemplo secuencial, donde realizamos una operación monádica en una colección lo suficientemente grande. - - val list = (1 to 10000).toList - list.map(_ + 42) - -Para realizar la misma operación en paralelo, lo único que devemos incluir, es la invocación al método `par` en la colección secuencial `list`. Después de eso, es posible utilizar la misma colección paralelizada de la misma manera que normalmente la usariamos si fuera una colección secuencial. El ejemplo superior puede ser paralelizado al hacer simplemente lo siguiente: - - list.par.map(_ + 42) - -El diseño de la librería de colecciones paralelizadas de Scala está inspirada y fuertemente integrada con la librería estandar de colecciones (secuenciales) del lenguaje (introducida en la versión 2.8). Se provee te una contraparte paralelizada a un número importante de estructuras de datos de la librería de colecciones (secuenciales) de Scala, incluyendo: - -* `ParArray` -* `ParVector` -* `mutable.ParHashMap` -* `mutable.ParHashSet` -* `immutable.ParHashMap` -* `immutable.ParHashSet` -* `ParRange` -* `ParTrieMap` (`collection.concurrent.TrieMap`s are new in 2.10) - -Además de una arquitectura común, la librería de colecciones paralelizadas de Scala también comparte la _extensibilidad_ con la librería de colecciones secuenciales. Es decir, de la misma manera que los usuarios pueden integrar sus propios tipos de tipos de colecciones de la librería normal de colecciones secuenciales, pueden realizarlo con la librería de colecciones paralelizadas, heredando automáticamente todas las operaciones paralelas disponibles en las demás colecciones paralelizadas de la librería estandar. - -## Algunos Ejemplos - -To attempt to illustrate the generality and utility of parallel collections, -we provide a handful of simple example usages, all of which are transparently -executed in parallel. - -De forma de ilustrar la generalidad y utilidad de las colecciones paralelizadas, proveemos un conjunto de ejemplos de uso útiles, todos ellos siendo ejecutados en paralelo de forma totalmente transparente al usuario. - -_Nota:_ Algunos de los siguientes ejemplos operan en colecciones pequeñas, lo cual no es recomendado. Son provistos como ejemplo para ilustrar solamente el propósito. Como una regla heurística general, los incrementos en velocidad de ejecución comienzan a ser notados cuando el tamaño de la colección es lo suficientemente grande, tipicamente algunos cuantos miles de elementos. (Para más información en la relación entre tamaño de una coleccion paralelizada y su performance, por favor véase [appropriate subsection]({{ site.baseurl}}/overviews/parallel-collections/performance.html#how_big_should_a_collection_be_to_go_parallel) en la sección [performance]({{ site.baseurl }}/overviews/parallel-collections/performance.html) (en inglés). - -#### map - -Usando un `map` paralelizado para transformar una colección de elementos tipo `String` a todos caracteres en mayúscula: - - scala> val apellidos = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par - apellidos: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) - - scala> apellidos.map(_.toUpperCase) - res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(SMITH, JONES, FRANKENSTEIN, BACH, JACKSON, RODIN) - -#### fold - -Sumatoria mediante `fold` en un `ParArray`: - - scala> val parArray = (1 to 1000000).toArray.par - parArray: scala.collection.parallel.mutable.ParArray[Int] = ParArray(1, 2, 3, ... - - scala> parArray.fold(0)(_ + _) - res0: Int = 1784293664 - -#### filtrando - - -Usando un filtrado mediante `filter` paralelizado para seleccionar los apellidos que alfabéticamente preceden la letra "K": - - scala> val apellidos = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par - apellidos: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) - - scala> apellidos.filter(_.head >= 'J') - res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Jackson, Rodin) - -## Creación de colecciones paralelizadas - -Las colecciones paralelizadas están pensadas para ser usadas exactamente de la misma manera que las colecciones secuenciales --la única diferencia notoria es cómo _obtener_ una colección paralelizada. - -Generalmente se tienen dos opciones para la creación de colecciones paralelizadas: - -Primero al utilizar la palabra clave `new` y una sentencia de importación apropiada: - - import scala.collection.parallel.immutable.ParVector - val pv = new ParVector[Int] - -Segundo, al _convertir_ desde una colección secuencial: - - val pv = Vector(1,2,3,4,5,6,7,8,9).par - -What's important to expand upon here are these conversion methods-- sequential -collections can be converted to parallel collections by invoking the -sequential collection's `par` method, and likewise, parallel collections can -be converted to sequential collections by invoking the parallel collection's -`seq` method. - -_Of Note:_ Collections that are inherently sequential (in the sense that the -elements must be accessed one after the other), like lists, queues, and -streams, are converted to their parallel counterparts by copying the elements -into a similar parallel collection. An example is `List`-- it's converted into -a standard immutable parallel sequence, which is a `ParVector`. Of course, the -copying required for these collection types introduces an overhead not -incurred by any other collection types, like `Array`, `Vector`, `HashMap`, etc. - -For more information on conversions on parallel collections, see the -[conversions]({{ site.baseurl }}/overviews/parallel-collections/converesions.html) -and [concrete parallel collection classes]({{ site.baseurl }}/overviews/parallel-collections/concrete-parallel-collections.html) -sections of thise guide. - -## Semantics - -While the parallel collections abstraction feels very much the same as normal -sequential collections, it's important to note that its semantics differs, -especially with regards to side-effects and non-associative operations. - -In order to see how this is the case, first, we visualize _how_ operations are -performed in parallel. Conceptually, Scala's parallel collections framework -parallelizes an operation on a parallel collection by recursively "splitting" -a given collection, applying an operation on each partition of the collection -in parallel, and re-"combining" all of the results that were completed in -parallel. - -These concurrent, and "out-of-order" semantics of parallel collections lead to -the following two implications: - -1. **Side-effecting operations can lead to non-determinism** -2. **Non-associative operations lead to non-determinism** - -### Side-Effecting Operations - -Given the _concurrent_ execution semantics of the parallel collections -framework, operations performed on a collection which cause side-effects -should generally be avoided, in order to maintain determinism. A simple -example is by using an accessor method, like `foreach` to increment a `var` -declared outside of the closure which is passed to `foreach`. - - scala> var sum = 0 - sum: Int = 0 - - scala> val list = (1 to 1000).toList.par - list: scala.collection.parallel.immutable.ParSeq[Int] = ParVector(1, 2, 3,… - - scala> list.foreach(sum += _); sum - res01: Int = 467766 - - scala> var sum = 0 - sum: Int = 0 - - scala> list.foreach(sum += _); sum - res02: Int = 457073 - - scala> var sum = 0 - sum: Int = 0 - - scala> list.foreach(sum += _); sum - res03: Int = 468520 - -Here, we can see that each time `sum` is reinitialized to 0, and `foreach` is -called again on `list`, `sum` holds a different value. The source of this -non-determinism is a _data race_-- concurrent reads/writes to the same mutable -variable. - -In the above example, it's possible for two threads to read the _same_ value -in `sum`, to spend some time doing some operation on that value of `sum`, and -then to attempt to write a new value to `sum`, potentially resulting in an -overwrite (and thus, loss) of a valuable result, as illustrated below: - - ThreadA: read value in sum, sum = 0 value in sum: 0 - ThreadB: read value in sum, sum = 0 value in sum: 0 - ThreadA: increment sum by 760, write sum = 760 value in sum: 760 - ThreadB: increment sum by 12, write sum = 12 value in sum: 12 - -The above example illustrates a scenario where two threads read the same -value, `0`, before one or the other can sum `0` with an element from their -partition of the parallel collection. In this case, `ThreadA` reads `0` and -sums it with its element, `0+760`, and in the case of `ThreadB`, sums `0` with -its element, `0+12`. After computing their respective sums, they each write -their computed value in `sum`. Since `ThreadA` beats `ThreadB`, it writes -first, only for the value in `sum` to be overwritten shortly after by -`ThreadB`, in effect completely overwriting (and thus losing) the value `760`. - -### Non-Associative Operations - -Given this _"out-of-order"_ semantics, also must be careful to perform only -associative operations in order to avoid non-determinism. That is, given a -parallel collection, `pcoll`, one should be sure that when invoking a -higher-order function on `pcoll`, such as `pcoll.reduce(func)`, the order in -which `func` is applied to the elements of `pcoll` can be arbitrary. A simple, -but obvious example is a non-associative operation such as subtraction: - - scala> val list = (1 to 1000).toList.par - list: scala.collection.parallel.immutable.ParSeq[Int] = ParVector(1, 2, 3,… - - scala> list.reduce(_-_) - res01: Int = -228888 - - scala> list.reduce(_-_) - res02: Int = -61000 - - scala> list.reduce(_-_) - res03: Int = -331818 - -In the above example, we take a `ParVector[Int]`, invoke `reduce`, and pass to -it `_-_`, which simply takes two unnamed elements, and subtracts the first -from the second. Due to the fact that the parallel collections framework spawns -threads which, in effect, independently perform `reduce(_-_)` on different -sections of the collection, the result of two runs of `reduce(_-_)` on the -same collection will not be the same. - -_Note:_ Often, it is thought that, like non-associative operations, non-commutative -operations passed to a higher-order function on a parallel -collection likewise result in non-deterministic behavior. This is not the -case, a simple example is string concatenation-- an associative, but non- -commutative operation: - - scala> val strings = List("abc","def","ghi","jk","lmnop","qrs","tuv","wx","yz").par - strings: scala.collection.parallel.immutable.ParSeq[java.lang.String] = ParVector(abc, def, ghi, jk, lmnop, qrs, tuv, wx, yz) - - scala> val alphabet = strings.reduce(_++_) - alphabet: java.lang.String = abcdefghijklmnopqrstuvwxyz - -The _"out of order"_ semantics of parallel collections only means that -the operation will be executed out of order (in a _temporal_ sense. That is, -non-sequentially), it does not mean that the result will be -re-"*combined*" out of order (in a _spatial_ sense). On the contrary, results -will generally always be reassembled _in order_-- that is, a parallel collection -broken into partitions A, B, C, in that order, will be reassembled once again -in the order A, B, C. Not some other arbitrary order like B, C, A. - -For more on how parallel collections split and combine operations on different -parallel collection types, see the [Architecture]({{ site.baseurl }}/overviews -/parallel-collections/architecture.html) section of this guide. - diff --git a/es/overviews/parallel-collections/overview.md b/es/overviews/parallel-collections/overview.md index 1bcb67d437..9c1dc6c609 100644 --- a/es/overviews/parallel-collections/overview.md +++ b/es/overviews/parallel-collections/overview.md @@ -165,14 +165,9 @@ En el ejemplo anterior, es posible para dos hilos leer el _mismo_ valor de `sum` Este ejemplo ilustra un escenario donde dos hilos leen el mismo valor, `0`, antes que el otro pueda sumar su parte de la ejecución sobre la colección paralela. En este caso el `HiloA` lee `0` y le suma el valor de su cómputo, `0+760`, y en el caso del `HiloB`, le suma al valor leido `0` su resultado, quedando `0+12`. Después de computar sus respectivas sumas, ambos escriben el valor en `sum`. Ya que el `HiloA` llega a escribir antes que el `HiloB` (por nada en particular, solamente coincidencia que en este caso llegue primero el `HiloA`), su valor se pierde, porque seguidamente llega a escribir el `HiloB` y borra el valor previamente guardado. Esto se llama _condición de carrera_ porque el valor termina resultando una cuestión de suerte, o aleatoria, de quién llega antes o después a escribir el valor final. -### Non-Associative Operations +### Operaciones no asociativas -Given this _"out-of-order"_ semantics, also must be careful to perform only -associative operations in order to avoid non-determinism. That is, given a -parallel collection, `pcoll`, one should be sure that when invoking a -higher-order function on `pcoll`, such as `pcoll.reduce(func)`, the order in -which `func` is applied to the elements of `pcoll` can be arbitrary. A simple, -but obvious example is a non-associative operation such as subtraction: +Dado este funcionamiento "fuera de orden", también se debe ser cuidadoso de realizar solo operaciones asociativas para evitar comportamientos no esperados. Es decir, dada una colección paralelizada `par_col`, uno debe saber que cuando invoca una función de orden superior sobre `par_col`, tal como `par_col.reduce(func)`, el orden en que la función `func` es invocada sobre los elementos de `par_col` puede ser arbitrario (de hecho, es el caso más probable). Un ejemplo simple y pero no tan obvio de una operación no asociativa es es una substracción: scala> val list = (1 to 1000).toList.par list: scala.collection.parallel.immutable.ParSeq[Int] = ParVector(1, 2, 3,… @@ -185,35 +180,18 @@ but obvious example is a non-associative operation such as subtraction: scala> list.reduce(_-_) res03: Int = -331818 - -In the above example, we take a `ParVector[Int]`, invoke `reduce`, and pass to -it `_-_`, which simply takes two unnamed elements, and subtracts the first -from the second. Due to the fact that the parallel collections framework spawns -threads which, in effect, independently perform `reduce(_-_)` on different -sections of the collection, the result of two runs of `reduce(_-_)` on the -same collection will not be the same. - -_Note:_ Often, it is thought that, like non-associative operations, non-commutative -operations passed to a higher-order function on a parallel -collection likewise result in non-deterministic behavior. This is not the -case, a simple example is string concatenation-- an associative, but non- -commutative operation: + +En el ejemplo anterior invocamos reduce sobre un `ParVector[Int]` pasándole `_-_`. Lo que hace esto es simplemente tomar dos elementos y resta el primero al segundo. Dado que el framework de colecciones paralelizadas crea varios hilos que realizan `reduce(_-_)` independientemente en varias secciones de la colección, el resultado de correr dos veces el método `reduce(_-_)` en la misma colección puede no ser el mismo. + +_Nota:_ Generalmente se piensa que, al igual que las operaciones no asociativas, las operaciones no conmutativas pasadas a un función de orden superior también generan resultados extraños (no deterministas). En realidad esto no es así, un simple ejemplo es la concatenación de Strings (cadenas de caracteres). -- una operación asociativa, pero no conmutativa: scala> val strings = List("abc","def","ghi","jk","lmnop","qrs","tuv","wx","yz").par strings: scala.collection.parallel.immutable.ParSeq[java.lang.String] = ParVector(abc, def, ghi, jk, lmnop, qrs, tuv, wx, yz) - scala> val alphabet = strings.reduce(_++_) - alphabet: java.lang.String = abcdefghijklmnopqrstuvwxyz - -The _"out of order"_ semantics of parallel collections only means that -the operation will be executed out of order (in a _temporal_ sense. That is, -non-sequentially), it does not mean that the result will be -re-"*combined*" out of order (in a _spatial_ sense). On the contrary, results -will generally always be reassembled _in order_-- that is, a parallel collection -broken into partitions A, B, C, in that order, will be reassembled once again -in the order A, B, C. Not some other arbitrary order like B, C, A. + scala> val alfabeto = strings.reduce(_++_) + alfabeto: java.lang.String = abcdefghijklmnopqrstuvwxyz -For more on how parallel collections split and combine operations on different -parallel collection types, see the [Architecture]({{ site.baseurl }}/overviews -/parallel-collections/architecture.html) section of this guide. +Lo que implica el "fuera de orden" en las colecciones paralelizadas es solamente que la operación será ejecutada fuera de orden (en un sentido _temporal_, es decir no secuencial, no significa que el resultado va a ser re-"*combinado*" fuera de orden (en un sentido de _espacio_). Al contrario, en general los resultados siempre serán reensamblados en roden, es decir una colección paralelizada que se divide en las siguientes particiones A, B, C, en ese orden, será reensamblada nuevamente en el orden A, B, C. No en otro orden arbitrario como B, C, A. +Para más información de cómo se dividen y se combinan los diferentes tipos de colecciones paralelizadas véase el artículo sobre [Arquitectura]({{ site.baseurl }}/es/overviews +/parallel-collections/architecture.html) de esta misma serie. diff --git a/overviews/parallel-collections/es/overview.md b/overviews/parallel-collections/es/overview.md deleted file mode 100644 index d0f379d6d3..0000000000 --- a/overviews/parallel-collections/es/overview.md +++ /dev/null @@ -1,244 +0,0 @@ ---- -layout: overview-large -title: Overview - -disqus: true - -partof: parallel-collections -num: 1 -languages: [ja] ---- - -**Autores originales: Aleksandar Prokopec, Heather Miller** - -**Traducción y arreglos: Santiago Basulto** - -## Motivación - -En el medio del cambio en los recientes años de los fabricantes de procesadores de arquitecturas simples a arquitecturas multi-nucleo, tanto el ámbito académico, como el industrial coinciden que la _Programación Paralela_ sigue siendo un gran desafío. - -Las Colecciones Paralelizadas (Parallel collections, en inglés) fueron incluidas en la librería del lenguaje Scala en un esfuerzo de facilitar la programación paralela al abstraer a los usuarios de detalles de paralelización de bajo nivel, mientras se provee con una abstracción de alto nivel, simple y familiar. La esperanza era, y sigue siendo, que el paralelismo implícito detrás de una abstracción de colecciones (como lo es el actual framework de colecciones del lenguaje) acercara la ejecución paralela confiable, un poco más al trabajo diario de los desarrolladores. - -La idea es simple: las colecciones son abstracciones de programación ficientemente entendidas y a su vez son frecuentemente usadas. Dada su regularidad, es posible que sean paralelizadas eficiente y transparentemente. Al permitirle al usuario intercambiar colecciones secuenciales por aquellas que son operadas en paralelo, las colecciones paralelizadas de Scala dan un gran paso hacia la posibilidad de que el paralelismo sea introducido cada vez más frecuentemente en nuestro código. - -Veamos el siguiente ejemplo secuencial, donde realizamos una operación monádica en una colección lo suficientemente grande. - - val list = (1 to 10000).toList - list.map(_ + 42) - -Para realizar la misma operación en paralelo, lo único que devemos incluir, es la invocación al método `par` en la colección secuencial `list`. Después de eso, es posible utilizar la misma colección paralelizada de la misma manera que normalmente la usariamos si fuera una colección secuencial. El ejemplo superior puede ser paralelizado al hacer simplemente lo siguiente: - - list.par.map(_ + 42) - -El diseño de la librería de colecciones paralelizadas de Scala está inspirada y fuertemente integrada con la librería estandar de colecciones (secuenciales) del lenguaje (introducida en la versión 2.8). Se provee te una contraparte paralelizada a un número importante de estructuras de datos de la librería de colecciones (secuenciales) de Scala, incluyendo: - -* `ParArray` -* `ParVector` -* `mutable.ParHashMap` -* `mutable.ParHashSet` -* `immutable.ParHashMap` -* `immutable.ParHashSet` -* `ParRange` -* `ParTrieMap` (`collection.concurrent.TrieMap`s are new in 2.10) - -Además de una arquitectura común, la librería de colecciones paralelizadas de Scala también comparte la _extensibilidad_ con la librería de colecciones secuenciales. Es decir, de la misma manera que los usuarios pueden integrar sus propios tipos de tipos de colecciones de la librería normal de colecciones secuenciales, pueden realizarlo con la librería de colecciones paralelizadas, heredando automáticamente todas las operaciones paralelas disponibles en las demás colecciones paralelizadas de la librería estandar. - -## Algunos Ejemplos - -To attempt to illustrate the generality and utility of parallel collections, -we provide a handful of simple example usages, all of which are transparently -executed in parallel. - -De forma de ilustrar la generalidad y utilidad de las colecciones paralelizadas, proveemos un conjunto de ejemplos de uso útiles, todos ellos siendo ejecutados en paralelo de forma totalmente transparente al usuario. - -_Nota:_ Algunos de los siguientes ejemplos operan en colecciones pequeñas, lo cual no es recomendado. Son provistos como ejemplo para ilustrar solamente el propósito. Como una regla heurística general, los incrementos en velocidad de ejecución comienzan a ser notados cuando el tamaño de la colección es lo suficientemente grande, tipicamente algunos cuantos miles de elementos. (Para más información en la relación entre tamaño de una coleccion paralelizada y su performance, por favor véase [appropriate subsection]({{ site.baseurl}}/overviews/parallel-collections/performance.html#how_big_should_a_collection_be_to_go_parallel) en la sección [performance]({{ site.baseurl }}/overviews/parallel-collections/performance.html) (en inglés). - -#### map - -Usando un `map` paralelizado para transformar una colección de elementos tipo `String` a todos caracteres en mayúscula: - - scala> val apellidos = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par - apellidos: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) - - scala> apellidos.map(_.toUpperCase) - res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(SMITH, JONES, FRANKENSTEIN, BACH, JACKSON, RODIN) - -#### fold - -Sumatoria mediante `fold` en un `ParArray`: - - scala> val parArray = (1 to 1000000).toArray.par - parArray: scala.collection.parallel.mutable.ParArray[Int] = ParArray(1, 2, 3, ... - - scala> parArray.fold(0)(_ + _) - res0: Int = 1784293664 - -#### filtrando - - -Usando un filtrado mediante `filter` paralelizado para seleccionar los apellidos que alfabéticamente preceden la letra "K": - - scala> val apellidos = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par - apellidos: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) - - scala> apellidos.filter(_.head >= 'J') - res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Jackson, Rodin) - -## Creación de colecciones paralelizadas - -Las colecciones paralelizadas están pensadas para ser usadas exactamente de la misma manera que las colecciones secuenciales --la única diferencia notoria es cómo _obtener_ una colección paralelizada. - -Generalmente se tienen dos opciones para la creación de colecciones paralelizadas: - -Primero al utilizar la palabra clave `new` y una sentencia de importación apropiada: - - import scala.collection.parallel.immutable.ParVector - val pv = new ParVector[Int] - -Segundo, al _convertir_ desde una colección secuencial: - - val pv = Vector(1,2,3,4,5,6,7,8,9).par - -What's important to expand upon here are these conversion methods-- sequential -collections can be converted to parallel collections by invoking the -sequential collection's `par` method, and likewise, parallel collections can -be converted to sequential collections by invoking the parallel collection's -`seq` method. - -_Of Note:_ Collections that are inherently sequential (in the sense that the -elements must be accessed one after the other), like lists, queues, and -streams, are converted to their parallel counterparts by copying the elements -into a similar parallel collection. An example is `List`-- it's converted into -a standard immutable parallel sequence, which is a `ParVector`. Of course, the -copying required for these collection types introduces an overhead not -incurred by any other collection types, like `Array`, `Vector`, `HashMap`, etc. - -For more information on conversions on parallel collections, see the -[conversions]({{ site.baseurl }}/overviews/parallel-collections/converesions.html) -and [concrete parallel collection classes]({{ site.baseurl }}/overviews/parallel-collections/concrete-parallel-collections.html) -sections of thise guide. - -## Semantics - -While the parallel collections abstraction feels very much the same as normal -sequential collections, it's important to note that its semantics differs, -especially with regards to side-effects and non-associative operations. - -In order to see how this is the case, first, we visualize _how_ operations are -performed in parallel. Conceptually, Scala's parallel collections framework -parallelizes an operation on a parallel collection by recursively "splitting" -a given collection, applying an operation on each partition of the collection -in parallel, and re-"combining" all of the results that were completed in -parallel. - -These concurrent, and "out-of-order" semantics of parallel collections lead to -the following two implications: - -1. **Side-effecting operations can lead to non-determinism** -2. **Non-associative operations lead to non-determinism** - -### Side-Effecting Operations - -Given the _concurrent_ execution semantics of the parallel collections -framework, operations performed on a collection which cause side-effects -should generally be avoided, in order to maintain determinism. A simple -example is by using an accessor method, like `foreach` to increment a `var` -declared outside of the closure which is passed to `foreach`. - - scala> var sum = 0 - sum: Int = 0 - - scala> val list = (1 to 1000).toList.par - list: scala.collection.parallel.immutable.ParSeq[Int] = ParVector(1, 2, 3,… - - scala> list.foreach(sum += _); sum - res01: Int = 467766 - - scala> var sum = 0 - sum: Int = 0 - - scala> list.foreach(sum += _); sum - res02: Int = 457073 - - scala> var sum = 0 - sum: Int = 0 - - scala> list.foreach(sum += _); sum - res03: Int = 468520 - -Here, we can see that each time `sum` is reinitialized to 0, and `foreach` is -called again on `list`, `sum` holds a different value. The source of this -non-determinism is a _data race_-- concurrent reads/writes to the same mutable -variable. - -In the above example, it's possible for two threads to read the _same_ value -in `sum`, to spend some time doing some operation on that value of `sum`, and -then to attempt to write a new value to `sum`, potentially resulting in an -overwrite (and thus, loss) of a valuable result, as illustrated below: - - ThreadA: read value in sum, sum = 0 value in sum: 0 - ThreadB: read value in sum, sum = 0 value in sum: 0 - ThreadA: increment sum by 760, write sum = 760 value in sum: 760 - ThreadB: increment sum by 12, write sum = 12 value in sum: 12 - -The above example illustrates a scenario where two threads read the same -value, `0`, before one or the other can sum `0` with an element from their -partition of the parallel collection. In this case, `ThreadA` reads `0` and -sums it with its element, `0+760`, and in the case of `ThreadB`, sums `0` with -its element, `0+12`. After computing their respective sums, they each write -their computed value in `sum`. Since `ThreadA` beats `ThreadB`, it writes -first, only for the value in `sum` to be overwritten shortly after by -`ThreadB`, in effect completely overwriting (and thus losing) the value `760`. - -### Non-Associative Operations - -Given this _"out-of-order"_ semantics, also must be careful to perform only -associative operations in order to avoid non-determinism. That is, given a -parallel collection, `pcoll`, one should be sure that when invoking a -higher-order function on `pcoll`, such as `pcoll.reduce(func)`, the order in -which `func` is applied to the elements of `pcoll` can be arbitrary. A simple, -but obvious example is a non-associative operation such as subtraction: - - scala> val list = (1 to 1000).toList.par - list: scala.collection.parallel.immutable.ParSeq[Int] = ParVector(1, 2, 3,… - - scala> list.reduce(_-_) - res01: Int = -228888 - - scala> list.reduce(_-_) - res02: Int = -61000 - - scala> list.reduce(_-_) - res03: Int = -331818 - -In the above example, we take a `ParVector[Int]`, invoke `reduce`, and pass to -it `_-_`, which simply takes two unnamed elements, and subtracts the first -from the second. Due to the fact that the parallel collections framework spawns -threads which, in effect, independently perform `reduce(_-_)` on different -sections of the collection, the result of two runs of `reduce(_-_)` on the -same collection will not be the same. - -_Note:_ Often, it is thought that, like non-associative operations, non-commutative -operations passed to a higher-order function on a parallel -collection likewise result in non-deterministic behavior. This is not the -case, a simple example is string concatenation-- an associative, but non- -commutative operation: - - scala> val strings = List("abc","def","ghi","jk","lmnop","qrs","tuv","wx","yz").par - strings: scala.collection.parallel.immutable.ParSeq[java.lang.String] = ParVector(abc, def, ghi, jk, lmnop, qrs, tuv, wx, yz) - - scala> val alphabet = strings.reduce(_++_) - alphabet: java.lang.String = abcdefghijklmnopqrstuvwxyz - -The _"out of order"_ semantics of parallel collections only means that -the operation will be executed out of order (in a _temporal_ sense. That is, -non-sequentially), it does not mean that the result will be -re-"*combined*" out of order (in a _spatial_ sense). On the contrary, results -will generally always be reassembled _in order_-- that is, a parallel collection -broken into partitions A, B, C, in that order, will be reassembled once again -in the order A, B, C. Not some other arbitrary order like B, C, A. - -For more on how parallel collections split and combine operations on different -parallel collection types, see the [Architecture]({{ site.baseurl }}/overviews -/parallel-collections/architecture.html) section of this guide. -