From ab5c936cd981f437e46b3ee96fef8bc9226e69eb Mon Sep 17 00:00:00 2001 From: Anastasia Date: Sun, 22 Jan 2017 12:12:14 +0300 Subject: [PATCH 01/12] russian translations start --- overviews/parallel-collections/overview.md | 2 +- ru/overviews/parallel-collections/overview.md | 180 ++++++++++++++++++ 2 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 ru/overviews/parallel-collections/overview.md diff --git a/overviews/parallel-collections/overview.md b/overviews/parallel-collections/overview.md index 90b69e187b..7edb494ae6 100644 --- a/overviews/parallel-collections/overview.md +++ b/overviews/parallel-collections/overview.md @@ -5,7 +5,7 @@ title: Overview disqus: true partof: parallel-collections -languages: [ja, zh-cn, es] +languages: [ja, zh-cn, es, ru] num: 1 --- diff --git a/ru/overviews/parallel-collections/overview.md b/ru/overviews/parallel-collections/overview.md new file mode 100644 index 0000000000..1c0d3accf4 --- /dev/null +++ b/ru/overviews/parallel-collections/overview.md @@ -0,0 +1,180 @@ +--- +layout: overview-large +title: Обзор + +disqus: true + +partof: parallel-collections +num: 1 +language: ru +--- + +**Авторы оригинала: Aleksandar Prokopec, Heather Miller** + +**Перевод Анастасии Маркиной** + +## Мотивация + +Пока производители процессоров в последние годы дружно переходили от одноядерных к многоядерным архитектурам, научное и производственное сообщества не менее дружно признали, что навыки параллельного программирования по-прежнему трудно привить широким массам. + +В попытке помочь в программировании многопоточности в стандартную библиотеку Scala были включены параллельные коллекции, которые скрыли от пользователей низкоуровневые подробности параллелизации, дав им привычную высокоуровневую абстракцию. Надежда была (и остается) на то, что скрытая под уровнем абстракции параллельность позволит на шаг приблизиться к ситуации, когда среднестатистический разработчик будет повседневно использовать в работе надежно исполняемый параллельный код. + +Идея проста: коллекции -- хорошо понятная и часто используемая программистами абстракция. И в силу своей структурности, они могут быть эффективно стать параллельными, оставив эту трансформацию прозрачной. Позволив пользователю "подменить" последовательные коллекции на те, что обрабатываются параллельно, решение Scala делает большой шаг вперед к охвату большего количества кода возможностями параллельной обработки. + +Рассмотрим следующий пример, где мы исполняем монадическую операцию на некоторой большой последовательной коллекции: + + val list = (1 to 10000).toList + list.map(_ + 42) + +Чтобы выполнить ту же самую операцию параллельно, требуется просто вызвать метод `par` +на последовательной коллекции `list`. После этого можно работать с параллельной коллекцией так же, как и с последовательной. То есть, пример выше примет вид: + + list.par.map(_ + 42) + +Библиотека параллельных коллекций Scala тесно связана с "последовательной" библиотекой коллекций Scala (представлена в версии 2.8), во многом потому, что последняя служила вдохновением к ее дизайну. Он предоставляет параллельную "ответную часть" к ряду важных структур данных из библиотеки (последовательных) коллекций Scala, в том числе: + +* `ParArray` +* `ParVector` +* `mutable.ParHashMap` +* `mutable.ParHashSet` +* `immutable.ParHashMap` +* `immutable.ParHashSet` +* `ParRange` +* `ParTrieMap` (`collection.concurrent.TrieMap` впервые в версии 2.10) + +В дополнение к общей архитектуре, библиотека параллельных коллекций Scala дополнительно делит со своей последовательной "половиной" _расширяемость_. Другими словами, как и в случае с обычными последовательными коллекциями, пользователи могут внедрять свои собственные типы коллекций, автоматически наследуя все предопределенные (параллельные) операции, доступные для других параллельных коллекций в стандартной библиотеке. + +## Несколько примеров + +Попробуем изобразить всеобщность и полезность представленных коллекций на ряде простых примеров, для каждого из которых характерно прозрачно-параллельное выполнение. + +_Примечание:_ Некоторые из последующих примеров оперируют небольшими коллекциями, для которых такой подход не рекомендуется. Они должны рассматриваться только как иллюстрация. Эвристически, ускорение становится заметным, когда размер коллекции дорастает до нескольких тысяч элементов. (Более подробно о взаимосвязи между размером коллекции и производительностью, смотрите [соответствующий подраздел]({{ site.baseurl}}/overviews/parallel-collections/performance.html#how_big_should_a_collection_be_to_go_parallel) раздела, посвященного [производительности]({{ site.baseurl }}/overviews/parallel-collections/performance.html) в данном руководстве.) + +#### map + +Используем параллельную `map` для преобразования набора строк `String` в верхний регистр: + + scala> val lastNames = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par + lastNames: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) + + scala> lastNames.map(_.toUpperCase) + res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(SMITH, JONES, FRANKENSTEIN, BACH, JACKSON, RODIN) + +#### fold + +Суммируем через `fold` на `ParArray`: + + scala> val parArray = (1 to 10000).toArray.par + parArray: scala.collection.parallel.mutable.ParArray[Int] = ParArray(1, 2, 3, ... + + scala> parArray.fold(0)(_ + _) + res0: Int = 50005000 + +#### filter + +Используем параллельный `filter` для отбора фамилий, которые начинаются с буквы "J" или стоящей дальше в алфавите: + + scala> val lastNames = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par + lastNames: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Frankenstein, Bach, Jackson, Rodin) + + scala> lastNames.filter(_.head >= 'J') + res0: scala.collection.parallel.immutable.ParSeq[String] = ParVector(Smith, Jones, Jackson, Rodin) + +## Создание параллельной коллекции + +Параллельные коллекции предназначались для того, чтобы быть использованными таким же образом, как и последовательные; единственное значимое отличие -- в методе _получения_ параллельной коллекции. + +В общем виде, есть два варианта создания параллельной коллекции: + +Первый, с использованием ключевого слова `new` и подходящего оператора import: + + import scala.collection.parallel.immutable.ParVector + val pv = new ParVector[Int] + +Второй, _преобразованием_ последовательной коллекции: + + val pv = Vector(1,2,3,4,5,6,7,8,9).par + +Разовьем эту важную мысль -- последовательные коллекции можно конвертировать в параллельные вызовом метода `par` на последовательных, и, соответственно, параллельные коллекции также можно конвертировать в последовательные вызовом метода `seq` на параллельных. + +_На заметку:_ Коллекции, являющиеся последовательными в силу наследования (в том смысле, что доступ к их элементам требуется получать по порядку, один элемент за другим), такие, как списки, очереди и потоки (streams), преобразовываются в свои параллельные аналоги копированием элементов в соответствующие параллельные коллекции. Например, список `List` конвертируется в стандартную неизменяемую параллельную последовательность, то есть в `ParVector`. Естественно, что копирование, которое для этого требуется, вносит дополнительный расход производительности, которого не требуют другие типы коллекций, такие как `Array`, `Vector`, `HashMap` и т.д. + +Больше информации о конвертировании можно найти в разделах [преобразования]({{ site.baseurl }}/overviews/parallel-collections/conversions.html) и [конкретные классы параллельных коллекций]({{ site.baseurl }}/overviews/parallel-collections/concrete-parallel-collections.html) этого руководства. + +## Семантика + +В то время, как абстракция параллельной коллекции заставляет думать о ней так, как если бы речь шла о нормальной последовательной коллекции, важно помнить, что семантика различается, особенно в том, что касается побочных эффектов и неассоциативных операций. + +Для того, чтобы увидеть, что происходит, для начала представим, _как именно_ операции выполняются параллельно. В концепции, когда фреймворк параллельных коллекций Scala распараллеливает операцию на соответствующей коллекции, он рекурсивно "разбивает" данную коллекцию, параллельно выполняет операцию на каждом разделе коллекции, а затем "комбинирует" все полученные результаты. + +Эти многопоточные, "неупорядоченные" семантики параллельных коллекций приводят к следующим скрытым следствиям: + +1. **Операции, производящие побочные эффекты, могут нарушать детерминизм** +2. **Неассоциативные операции могут нарушать детерминизм** + +### Операции, производящие побочные эффекты. + +Вследствие использования фреймворком параллельных коллекций семантики _многопоточного_ выполнения, в большинстве случаев для соблюдения детерминизма требуется избегать выполнения на коллекциях операций, которые выполняют побочные действия. В качестве простого примера попробуем использовать метод доступа `foreach` для увеличения значения переменной `var`, объявленной вне замыкания, которое было передано `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 + +В примере видно, что несмотря на то, что каждый раз `sum` инициализируется в 0, при каждом новом вызове `foreach` на предложенном `list`, `sum` получает различное значение. Источником недетерминизма является так называемая _гонка_-- параллельное чтение/запись одной и той же изменяемой переменной. + +В примере выше возможен случай, когда два потока прочитают _одно и то же_ значение переменной `sum`, потратят некоторое время на выполнение операции над этим значением `sum`, а потом попытаются записать новое значение в `sum`, что может привести к перезаписи (а следовательно, к потере) значимого результата, как показано ниже: + + Поток A: читает значение sum, sum = 0 значение sum: 0 + Поток B: читает значение sum, sum = 0 значение sum: 0 + Поток A: увеличивает sum на 760, пишет sum = 760 значение sum: 760 + Поток B: увеличивает sum на 12, пишет sum = 12 значение sum: 12 + +Приведенный выше пример демонстрирует сценарий, где два потока успевают прочитать одно и то же значение, `0`, прежде чем один или другой из них успеет прибавить к этому `0` элемент из своего куска параллельной коллекции. В этом случае `Поток A` читает `0` и прибавляет к нему свой элемент, `0+760`, в то время, как `Поток B` прибавляет `0` к своему элементу, `0+12`. После того, как они вычислили свои суммы, каждый из них записывает свое значение в `sum`. Получилось так, что `Поток A` успевает записать значение первым, только для того, чтобы это помещенное в `sum` значение было практически сразу же перезаписано потоком `B`, тем самым полностью перезаписав (и потеряв) значение `760`. + +### Неассоциативные операции + +Из-за _"неупорядоченной"_ семантики, нелишней осторожностью становится требование выполнять только ассоциативные операции во избежание недетерминированности. То есть, если мы имеем параллельную коллекцию `pcoll`, нужно убедиться, что при вызове на `pcoll` функции более высокого уровня, такой как `pcoll.reduce(func)`, порядок, в котором `func` применяется к элементам `pcoll`, может быть произвольным. Простым и очевидным примером неассоциативной операции является вычитание: + + 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 + +В примере выше, мы берем `ParVector[Int]`, вызываем функцию `reduce`, и передаем ей `_-_`, которая просто берет два неименованных элемента, и вычитает один из другого. Вследствие того, что фреймворк параллельных коллекций порождает потоки и независимо выполняет `reduce(_-_)` на разных частях коллекции, результат двух запусков `reduce(_-_)` на одной и той же коллекции не будет одним и тем же. + +_Примечание:_ Часто возникает мысль, что так же, как и в случае с неассоциативными, некоммутативные операции, переданные в более высокую уровнем функцию на параллельной коллекции, приводят к недетеминированному поведению. Это неверно, простой пример -- конкатенация строк -- ассоциативная, но некоммутативная операция: + + 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 + +_"Неупорядоченная"_ семантика параллельных коллекций означает только то, что операции будут выполнены не по порядку (во _временном_ отношении. То есть, не последовательно), она не означает, что результат будет "*перемешан*" относительно изначального порядка (в _пространственном_ отношении). Напротив, результат будет практически всегда пересобран _по-порядку_-- то есть, параллельная коллекция, разбитая на части в порядке A, B, C, будет снова объединена в том же порядке A, B, C, а не в каком-то произвольном, например, B, C, A. + +Если требуется больше информации о том, как разделяются и комбинируются операции на различных типах коллекций, посетите раздел [Архитектура]({{ site.baseurl }}/overviews/parallel-collections/architecture.html) этого руководства. + From 991152b0ad988b77bc2567f729396a233d043b94 Mon Sep 17 00:00:00 2001 From: Anastasia Date: Sun, 5 Feb 2017 14:24:31 +0300 Subject: [PATCH 02/12] small stylistic fix --- ru/overviews/parallel-collections/overview.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ru/overviews/parallel-collections/overview.md b/ru/overviews/parallel-collections/overview.md index 1c0d3accf4..e4c8da15f4 100644 --- a/ru/overviews/parallel-collections/overview.md +++ b/ru/overviews/parallel-collections/overview.md @@ -15,11 +15,11 @@ language: ru ## Мотивация -Пока производители процессоров в последние годы дружно переходили от одноядерных к многоядерным архитектурам, научное и производственное сообщества не менее дружно признали, что навыки параллельного программирования по-прежнему трудно привить широким массам. +Пока производители процессоров в последние годы дружно переходили от одноядерных к многоядерным архитектурам, научное и производственное сообщества не менее дружно признали, что многопоточное программирование по-прежнему трудно сделать популярным. -В попытке помочь в программировании многопоточности в стандартную библиотеку Scala были включены параллельные коллекции, которые скрыли от пользователей низкоуровневые подробности параллелизации, дав им привычную высокоуровневую абстракцию. Надежда была (и остается) на то, что скрытая под уровнем абстракции параллельность позволит на шаг приблизиться к ситуации, когда среднестатистический разработчик будет повседневно использовать в работе надежно исполняемый параллельный код. +Чтобы упростить написание многопоточных программ, в стандартную библиотеку Scala были включены параллельные коллекции, которые скрыли от пользователей низкоуровневые подробности параллелизации, дав им привычную высокоуровневую абстракцию. Надежда была (и остается) на то, что скрытая под уровнем абстракции параллельность позволит на шаг приблизиться к ситуации, когда среднестатистический разработчик будет повседневно использовать в работе надежно исполняемый параллельный код. -Идея проста: коллекции -- хорошо понятная и часто используемая программистами абстракция. И в силу своей структурности, они могут быть эффективно стать параллельными, оставив эту трансформацию прозрачной. Позволив пользователю "подменить" последовательные коллекции на те, что обрабатываются параллельно, решение Scala делает большой шаг вперед к охвату большего количества кода возможностями параллельной обработки. +Идея проста: коллекции -- хорошо понятная и часто используемая программистами абстракция. Упорядоченность коллекций позволяет эффективно и прозрачно (для пользователя) обрабатывать их параллельно. Позволив пользователю "подменить" последовательные коллекции на те, что обрабатываются параллельно, решение Scala делает большой шаг вперед к охвату большего количества кода возможностями параллельной обработки. Рассмотрим следующий пример, где мы исполняем монадическую операцию на некоторой большой последовательной коллекции: @@ -31,7 +31,7 @@ language: ru list.par.map(_ + 42) -Библиотека параллельных коллекций Scala тесно связана с "последовательной" библиотекой коллекций Scala (представлена в версии 2.8), во многом потому, что последняя служила вдохновением к ее дизайну. Он предоставляет параллельную "ответную часть" к ряду важных структур данных из библиотеки (последовательных) коллекций Scala, в том числе: +Библиотека параллельных коллекций Scala тесно связана с "последовательной" библиотекой коллекций Scala (представлена в версии 2.8), во многом потому, что последняя служила вдохновением к ее дизайну. Он предоставляет параллельную альтернативу ряду важных структур данных из библиотеки (последовательных) коллекций Scala, в том числе: * `ParArray` * `ParVector` @@ -42,7 +42,7 @@ language: ru * `ParRange` * `ParTrieMap` (`collection.concurrent.TrieMap` впервые в версии 2.10) -В дополнение к общей архитектуре, библиотека параллельных коллекций Scala дополнительно делит со своей последовательной "половиной" _расширяемость_. Другими словами, как и в случае с обычными последовательными коллекциями, пользователи могут внедрять свои собственные типы коллекций, автоматически наследуя все предопределенные (параллельные) операции, доступные для других параллельных коллекций в стандартной библиотеке. +Библиотека параллельных коллекций Scala _расширяема_ также как и последовательные коллекции, представленные в стандартной библиотеке. Другими словами, как и в случае с обычными последовательными коллекциями, пользователи могут внедрять свои собственные типы коллекций, автоматически наследуя все предопределенные (параллельные) операции, доступные для других параллельных коллекций в стандартной библиотеке. ## Несколько примеров @@ -109,10 +109,10 @@ _На заметку:_ Коллекции, являющиеся последов Эти многопоточные, "неупорядоченные" семантики параллельных коллекций приводят к следующим скрытым следствиям: -1. **Операции, производящие побочные эффекты, могут нарушать детерминизм** +1. **Операции, имеющие побочные эффекты, могут нарушать детерминизм** 2. **Неассоциативные операции могут нарушать детерминизм** -### Операции, производящие побочные эффекты. +### Операции, имеющие побочные эффекты. Вследствие использования фреймворком параллельных коллекций семантики _многопоточного_ выполнения, в большинстве случаев для соблюдения детерминизма требуется избегать выполнения на коллекциях операций, которые выполняют побочные действия. В качестве простого примера попробуем использовать метод доступа `foreach` для увеличения значения переменной `var`, объявленной вне замыкания, которое было передано `foreach`. From 7e5ee2ca89ce6a2233378295f4e5ed0b34981ae4 Mon Sep 17 00:00:00 2001 From: Anastasia Date: Sun, 5 Feb 2017 14:45:05 +0300 Subject: [PATCH 03/12] architecture subpart --- .../parallel-collections/architecture.md | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 ru/overviews/parallel-collections/architecture.md diff --git a/ru/overviews/parallel-collections/architecture.md b/ru/overviews/parallel-collections/architecture.md new file mode 100644 index 0000000000..e24fabb2df --- /dev/null +++ b/ru/overviews/parallel-collections/architecture.md @@ -0,0 +1,68 @@ +--- +layout: overview-large +title: Архитектура библиотеки параллельных коллекций + +disqus: true + +partof: parallel-collections +language: ru +num: 5 +--- + +Так же, как и обычная последовательная, параллельная библиотека коллекций Scala существует во множестве различных реализаций достаточно большого количества операций над коллекциями. И так же, как последовательная, библиотека избегает повторений кода путем реализации большинства операций посредством собственных "шаблонов", которые достаточно объявить один раз, а потом гибко наследовать в различных реализациях параллельных коллекций. + +Преимущества этого подхода сильно облегчают **поддержку** и **расширяемость**. Поддержка может стать простой и надежной, когда одна реализация операции над параллельной коллекцией наследуется всеми параллельными коллекциями; исправления ошибок в этом случае сами распространяются вниз по иерархии классов, а не требуют дублировать реализации. По тем же причинам всю библиотеку становится проще расширять-- новые классы коллекций могут просто унаследовать большинство имеющихся операций. + + +## Ключевые абстракции + +Упомянутые выше "шаблонные" трейты реализуют большинство параллельных операций в терминах двух ключевых абстракций -- разделителей (`Splitter`) и компоновщиков (`Combiner`). + +### Разделители + +Задача разделителя `Splitter`, как и предполагает имя, заключается в том, чтобы неочевидным образом разбить параллельную коллекцию на разделы. А основная идея-- в том, чтобы разбивать коллекцию на более мелкие части, пока их размер не станет подходящим для последовательной обработки. + + trait Splitter[T] extends Iterator[T] { + def split: Seq[Splitter[T]] + } + +Что интересно, разделители `Splitter` реализованы через итераторы-- `Iterator`, а это подразумевает, что помимо разделения, они позволяют фреймворку перебирать элементы параллельной коллекции (то есть, наследуют стандартные методы трейта `Iterator`, такие, как `next` и `hasNext`.) Уникальность этого "разделяющего итератора" в том, что его метод `split` разбивает текущий объект `this` (мы помним, что `Splitter`, это подтип `Iterator`а) на другие разделители `Splitter`, каждый из которых перебирает свой, **отделенный** набор элементов когда-то целой параллельной коллекции. И так же, как любой нормальный `Iterator`, `Splitter` становится недействительным после того, как вызван его метод `split`. + +Как правило, коллекции разделяются `Splitter`ами на подмножества примерно одинакового размера. В случаях, когда требуются разделы произвольного размера, особенно в параллельных последовательностях, используется `PreciseSplitter`, который является наследником `Splitter` и дополнительно реализует точный метод разделения, `psplit`. + +### Компоновщики + +Компоновщик `Combiner` можно представить себе как обобщенный `Builder` из библиотеки последовательных коллекций Scala. У каждой параллельной коллекции есть свой отдельный `Combiner`, так же, как у каждой последовательной есть свой `Builder`. + +Если в случае с последовательными коллекциями элементы можно добавлять в `Builder`, а потом получить коллекцию, вызвав метод `result`, то при работе с параллельными требуется вызвать у `Combiner`а метод `combine`, который берет аргументом другой `Combiner` и делает новый `Combiner`, который содержит объединенный набор элементов обоих компоновщиков. После вызова метода `combine` оба компоновщика становятся недействительными. + + trait Combiner[Elem, To] extends Builder[Elem, To] { + def combine(other: Combiner[Elem, To]): Combiner[Elem, To] + } + +Два параметра-типа в примере выше, `Elem` и `To`, просто обозначают тип элемента и тип результирующей коллекции соответственно. + +_Примечание:_ Если есть два `Combiner`а, `c1` и `c2` где `c1 eq c2` равняется `true` (то есть, они являются одним и тем же `Combiner`ом), вызов `c1.combine(c2)` всегда ничего не делает, а просто возвращает исходный `Combiner`, то есть `c1`. + + +## Иерархия + +Параллельные коллекции Scala во многом созданы под влиянием дизайна библиотеки (последовательных) коллекций Scala. На рисунке ниже показано, что их дизайн фактически отражает соответствующие трейты фреймворка обычных коллекций. + +[]({{ site.baseurl }}/resources/images/parallel-collections-hierarchy.png) + +
Иерархия библиотеки Scala: коллекции и параллельные коллекции
+
+ +Цель, конечно же, в том, чтобы интегрировать параллельные коллекции с последовательными настолько тесно, насколько это возможно, так, чтобы можно было без дополнительных усилий заменять последовательные коллекции параллельными (и наоборот). + +Чтобы можно было получить ссылку на коллекцию, которая может быть либо последовательной, либо параллельной (так, чтобы было возможно "переключаться" между параллельной и последовательной коллекции вызовами `par` и `seq` соответственно), у обоих типов коллекций должен быть общий предок. Этим источником "обобщенных" трейтов, как показано выше, являются `GenTraversable`, `GenIterable`, `GenSeq`, `GenMap` и `GenSet`, которые не гарантируют того, что элементы будут обрабатываться по-порядку или по-одному. Отсюда наследуются соответствующие последовательные и параллельные трейты; например, `ParSeq` и `Seq` являются подтипами общей последовательности `GenSeq`, а не унаследованы друг от друга. + +Более подробное обсуждение иерархии, разделяемой последовательными и параллельными коллекциями, можно найти в техническом отчете. \[[1][1]\] + + +## Ссылки + +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" From 5680c035601518199fe285b619a0cb9b123de9cc Mon Sep 17 00:00:00 2001 From: Anastasia Date: Sat, 18 Feb 2017 09:22:55 +0300 Subject: [PATCH 04/12] two more subparts --- .../parallel-collections/conversions.md | 51 ++++++++ ru/overviews/parallel-collections/ctries.md | 121 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 ru/overviews/parallel-collections/conversions.md create mode 100644 ru/overviews/parallel-collections/ctries.md diff --git a/ru/overviews/parallel-collections/conversions.md b/ru/overviews/parallel-collections/conversions.md new file mode 100644 index 0000000000..61d3754814 --- /dev/null +++ b/ru/overviews/parallel-collections/conversions.md @@ -0,0 +1,51 @@ +--- +layout: overview-large +title: Преобразования параллельных коллекций + +disqus: true + +partof: parallel-collections +language: ru +num: 3 +--- + +## Взаимные преобразования последовательных и параллельных коллекций + +Любая последовательная коллекция может быть преобразована в свою параллельную альтернативу вызовом метода `par`, причем некоторые типы последовательных коллекций имеют прямой параллельный аналог. Для таких коллекций конвертация эффективна-- она занимает постоянное время, так как и последовательная и параллельная коллекция представлены одной и той же структурой данных (за исключением изменяемых хэш-таблиц и хэш-множеств, преобразование которых требует больше времени в первый вызов метода `par`, тогда как последующие вызовы `par` занимают постоянное время). Нужно заметить, что если изменяемые коллекции делят одну лежащую в основе структуру данных, то изменения, сделанные в последовательной коллекции, будут видны в ее параллельной ответной части. + +| Последовательные | Параллельные | +| ---------------- | -------------- | +| **изменяемые** | | +| `Array` | `ParArray` | +| `HashMap` | `ParHashMap` | +| `HashSet` | `ParHashSet` | +| `TrieMap` | `ParTrieMap` | +| **неизменяемые** | | +| `Vector` | `ParVector` | +| `Range` | `ParRange` | +| `HashMap` | `ParHashMap` | +| `HashSet` | `ParHashSet` | + +Другие коллекции, такие, как списки, очереди или потоки, последовательны по своей сути, в том смысле, что элементы должны выбираться один за другим. Такие коллекции преобразуются в свои параллельные альтернативы копированием элементов в схожую параллельную коллекцию. Например, рекурсивный список (functional list) преобразуется в стандартную неизменяемую параллельную последовательность, то есть в параллельный вектор. + +Любая параллельная коллекция может быть преобразована в её последовательный вариант вызовом метода `seq`. Конвертирование параллельной коллекции в последовательную эффективно всегда-- оно занимает постоянное время. Вызов `seq` на изменяемой параллельной коллекции возвращает последовательную, которая отображает ту же область памяти-- изменения, сделанные в одной коллекции, будут видимы в другой. + +## Преобразования между различными типами коллекций + +Параллельные коллекции могут конвертироваться в другие типы коллекций, не теряя при этом своей параллельности. Например, вызов метода `toSeq` последовательное множество преобразует в обычную последовательность, а параллельное-- в параллельную. Общий принцип такой: если есть параллельный вариант коллекции `X`, то метод `toX` преобразует коллекцию к типу `ParX`. + +Ниже приведена сводная таблица всех методов преобразования: + +| Метод | Тип возвращаемого значения | +| -------------- | -------------------------- | +| `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/ru/overviews/parallel-collections/ctries.md b/ru/overviews/parallel-collections/ctries.md new file mode 100644 index 0000000000..e495793200 --- /dev/null +++ b/ru/overviews/parallel-collections/ctries.md @@ -0,0 +1,121 @@ +--- +layout: overview-large +title: Многопоточные нагруженные деревья + +disqus: true + +partof: parallel-collections +language: ru +num: 4 +--- + +Большинство многопоточных структур данных не гарантирует правильности последовательного перебора элементов в случае, если эта структура изменяется во время прохождения. То же верно, кстати, и в случае большинства изменяемых коллекций. Особенность многопоточных нагруженных деревьев (также известных, как префиксные деревья) -- `tries`-- заключается в том, что они позволяют модифицировать само дерево, которое в данный момент просматривается. Сделанные изменения становятся видимыми только при следующем прохождении. Так ведут себя и последовательные нагруженные деревья, и их параллельные аналоги; единственное отличие-- в том, что первые перебирают элементы последовательно, а вторые-- параллельно. + +Это замечательное свойство позволяет упростить ряд алгоритмов. Обычно это такие алгоритмы, в которых некоторый набор данных обрабатывается итеративно, причем для обработки различных элементов требуется различное количество итераций. + +В следующем примере вычисляются квадратные корни некоторого набор чисел. Каждая итерация обновляет значение квадратного корня. Числа, квадратные корни которых достигли необходимой точности, исключаются из перебираемого набора. + + case class Entry(num: Double) { + var sqrt = num + } + + val length = 50000 + + // готовим исходные данные + val entries = (1 until length) map { num => Entry(num.toDouble) } + val results = ParTrieMap() + for (e <- entries) results += ((e.num, e)) + + // вычисляем квадратные корни + 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 + } + } + +Отметим, что в приведенном выше вычислении квадратных корней вавилонским методом (\[[3][3]\]) некоторые значения могут сойтись гораздо быстрее, чем остальные. По этой причине мы исключаем их из `results`, чтобы перебирались только те элементы, которые нуждаются в дальнейшей обработке. + +Другим примером является алгоритм поиска в ширину, который итеративно расширяет очередь перебираемых узлов до тех пор, пока или не будет найден целевой узел, или не закончатся узлы, за счет которых можно расширить поиск. Определим точку на двухмерной карте как кортеж значений `Int`. Обозначим как `map` двухмерный массив булевых значений, которые обозначают, занята соответствующая ячейка или нет. Затем объявим два многопоточных дерева-- `open`, которое содержит все точки, которые требуется раскрыть, и `closed`, в котором хранятся уже обработанные точки. Мы намерены начать поиск с углов карты и найти путь к центру-- инициализируем ассоциативный массив `open` подходящими точками. Затем будем раскрывать параллельно все точки, содержащиеся в ассоциативном массиве `open` до тех пор, пока больше не останется точек. Каждый раз, когда точка раскрывается, она удаляется из массива `open` и помещается в массив `closed`. + +Выполнив все это, выведем путь от целевого до стартового узла. + + val length = 1000 + + // объявляем тип Node + type Node = (Int, Int); + type Parent = (Int, Int); + + // операции над типом Node + 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); + + // создаем карту и целевую точку + 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 - фронт обработки + // список closed - уже обработанные точки + val open = ParTrieMap[Node, Parent]() + val closed = ParTrieMap[Node, Parent]() + + // добавляем несколько стартовых позиций + open((0, 0)) = null + open((length - 1, length - 1)) = null + open((0, length - 1)) = null + open((length - 1, 0)) = null + + // "жадный" поиск в ширину + 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) + } + } + + // выводим путь + var pathnode = open(target) + while (closed.contains(pathnode)) { + print(pathnode + "->") + pathnode = closed(pathnode) + } + println() + +На GitHub есть пример реализации игры "Жизнь", который использует многопоточные хэш-деревья-- `Ctries`, чтобы выборочно симулировать только те части механизма игры, которые в настоящий момент активны \[[4][4]\]. +Он также включает в себя основанную на `Swing` визуализацию, которая позволяет посмотреть, как подстройка параметров влияет на производительность. + +Многопоточные нагруженные деревья также поддерживают атомарную, неблокирующую операцию `snapshot`, выполнение которой осуществляется за постоянное время. Эта операция создает новое многопоточное дерево со всеми элементами на некоторый выбранный момент времени, создавая таким образом снимок состояния дерева в этот момент. +На самом деле, операция `snapshot` просто создает новый корень дерева. Последующие изменения отложенно перестраивают ту часть многопоточного дерева, которая соответствует изменению, и оставляет нетронутой ту часть, которая не изменилась. Прежде всего это означает, что операция 'snapshot' сама по себе не затратна, так как не происходит копирования элементов. Кроме того, так как оптимизация "копирования при записи" создает копии только измененных частей дерева, последующие модификации горизонтально масштабируемы. +Метод `readOnlySnapshot` чуть более эффективен, чем метод `snapshot`, но он возвращает неизменяемый ассоциативный массив, который доступен только для чтения. Многопоточные деревья также поддерживают атомарную операцию постоянного времени `clear`, основанную на рассмотренном механизме снимков. +Чтобы подробнее узнать о том, как работают многопоточные деревья и их снимки, смотрите \[[1][1]\] и \[[2][2]\]. + +На рассмотренном механизме снимков основана работа итераторов многопоточных деревьев. Прежде чем будет создан объект-итератор, берется снимок многопоточного дерева. Таким образом, итератор перебирает только те элементы дерева, которые присутствовали на момент создания снимка. Фактически, итераторы используют те снимки, которые дают доступ только на чтение. + +На том же механизме снимков основана операция `size`. В качестве примитивной реализации этой операции можно просто создать итератор (то есть, снимок) и перебрать все элементы, подсчитывая их. Таким образом, каждый вызов операции `size` будет требовать времени, прямо пропорционального числу элементов. Однако, многопоточные деревья в целях оптимизации кэшируют размеры своих отдельных частей, тем самым уменьшая временную сложность метода `size` до амортизированно-логарифмической. В результате получается, что если вызвать метод `size` один раз, можно осуществлять последующие вызовы `size` затрачивая минимум ресурсов, вычисляя, как правило, размеры только тех частей, которые изменились после последнего вызова `size`. Кроме того, вычисление размера параллельных многопоточных деревьев выполняется параллельно. + + +## Ссылки + +1. ["Cache-Aware" неблокирующие многопоточные хэш-деревья][1] +2. [Многопоточные деревья, поддерживающие эффективные неблокирующие снимки][2] +3. [Методы вычисления квадратных корней][3] +4. [Симуляция игры "Жизнь"][4] + + [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" + [4]: https://github.com/axel22/ScalaDays2012-TrieMap "game-of-life-ctries" From 899f850aec6bff663f08549bf1b93aa0e7c5ce83 Mon Sep 17 00:00:00 2001 From: Anastasia Date: Sat, 11 Mar 2017 09:53:05 +0300 Subject: [PATCH 05/12] concrete and custom parallel collections --- .../concrete-parallel-collections.md | 165 +++++++++++++++ .../custom-parallel-collections.md | 195 ++++++++++++++++++ 2 files changed, 360 insertions(+) create mode 100644 ru/overviews/parallel-collections/concrete-parallel-collections.md create mode 100644 ru/overviews/parallel-collections/custom-parallel-collections.md diff --git a/ru/overviews/parallel-collections/concrete-parallel-collections.md b/ru/overviews/parallel-collections/concrete-parallel-collections.md new file mode 100644 index 0000000000..20252f1e9a --- /dev/null +++ b/ru/overviews/parallel-collections/concrete-parallel-collections.md @@ -0,0 +1,165 @@ +--- +layout: overview-large +title: Конкретные классы параллельных коллекций + +disqus: true + +partof: parallel-collections +language: ru +num: 2 +--- + +## Параллельный Массив + +Последовательность [ParArray](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/parallel/mutable/ParArray.html) хранит линейный массив смежно хранимых элементов. Это означает, что получение доступа и обновление элементов эффективно, так как происходит путем изменения массива, лежащего в основе. По этой причине наиболее эффективна последовательная обработка элементов одного за другим. Параллельные массивы похожи на обычные в том отношении, что их размер постоянен. + + 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,... + +Реализация разбивки параллельного массива [разделителями]({{ site.baseurl }}/ru/overviews/parallel-collections/architecture.html#core_abstractions) сводится к созданию двух новых разделителей с последующим обновлением их итерационных индексов. [Компоновщики]({{ site.baseurl }}/ru/overviews/parallel-collections/architecture.html#core_abstractions) играют более заметную роль. Так как мы не знаем заранее количество элементов (и следовательно, размер массива) при выполнении большинства методов трансформации (например, `flatMap`, `filter`, `takeWhile`, и т.д.), каждый компоновщик, в сущности, является массивом-буфером, у которого операция `+=` требует для выполнения амортизированное постоянное время. Разные процессоры добавляют элементы к отдельным компоновщикам параллельного массива, которые потом по цепочке объединяют свои внутренние массивы. Лежащий в основе массив размещается и параллельно заполняется только после того, как становится известным общее число элементов. По этой причине методы трансформации требуют больше ресурсов, чем методы получения доступа. Также стоит заметить, что финальное размещение массива выполняется JVM последовательно, поэтому этот момент может стать узким местом, если даже сама операция отображения весьма нересурсоемкая. + +Вызов метода `seq` приводит к преобразованию параллельного массива в коллекцию `ArraySeq`, которая является его последовательным аналогом. Такое преобразование эффективно, и в основе `ArraySeq` остается тот же массив, что и был у исходного параллельного. + +## Параллельный вектор + +[ParVector](http://www.scala-lang.org/api/{{ site.scala-version}}/scala/collection/parallel/immutable/ParVector.html) является неизменяемой последовательностью, временная сложность доступа и обновления которой является логарифмической с низкой константой-множителем. + + 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,... + +Неизменяемые векторы представлены 32-ичными деревьями (32-way trees), поэтому [разделители]({{ site.baseurl }}/ru/overviews/parallel-collections/architecture.html#core_abstractions) разбивают их, назначая по поддереву каждому новому разделителю. +[Компоновщики]({{ site.baseurl }}/ru/overviews/parallel-collections/architecture.html#core_abstractions) в настоящий момент хранят вектор из элементов и компонуют путем отложенного копирования. По этой причине методы трансформации менее масштабируемы по сравнению с теми же методами параллельного массива. Как только в будущем релизе Scala станет доступной операция конкатенации векторов, компоновщики станут образовываться путем конкатенации, и от этого методы трансформации станут гораздо более эффективными. + +Параллельный вектор является параллельным аналогом последовательной коллекции [Vector](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/immutable/Vector.html), и преобразования одного в другое занимают постоянное время. + +## Параллельный диапазон + +[ParRange](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/parallel/immutable/ParRange.html) представляет собой упорядоченную последовательность элементов, отстоящих друг от друга на одинаковые промежутки. Параллельный диапазон создается подобно последовательному [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) + +Подобно тому, как последовательные диапазоны не имеют строителей, параллельные диапазоны не имеют [компоновщиков]({{ site.baseurl }}/ru/overviews/parallel-collections/architecture.html#core_abstractions). При создании отображения (mapping) элементов параллельного диапазона получается параллельный вектор. Последовательные и параллельные диапазоны могут эффективно преобразовываться друг в друга вызовами методов `seq` и `par`. + +## Параллельные хэш-таблицы + +В основе параллельной хэш-таблицы лежит массив, причем место элемента таблицы в этом массиве определяется хэш-кодом элемента. На хэш-таблицах основаны параллельные изменяемые хэш-множества ([mutable.ParHashSet](http://www.scala-lang.org/api/{{ site.scala-version}}/scala/collection/parallel/mutable/ParHashSet.html)) и параллельные изменяемые ассоциативные хэш-массивы (хэш-отображения) ([mutable.ParHashMap](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/parallel/mutable/ParHashMap.html)). + + 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,... + +Компоновщики параллельных хэш-таблиц распределяют элементы по блокам в соответствии с префиксом их хэш-кода. Компонуют же они простой конкатенацией таких блоков. Когда хэш-таблица окажется окончательно сформированной (то есть, когда будет вызван метод `result` компоновщика), размещается лежащий в основе массив и элементы из различных блоков параллельно копируются в различные смежнолежащие сегменты этого массива. + +Последовательные хэш-отображения и хэш-множества могут преобразовываться в свои параллельные аналоги с помощью метода `par`. Внутри параллельной хэш-таблицы требуется поддерживать карту размеров, которая отслеживает количество элементов в различных ее частях. Это значит, что при первом преобразовании последовательной хэш-таблицы в параллельную, вся она просматривается с целью создания карты размеров - по этой причине первый вызов метода `par` требует линейного по отношению к числу элементов времени выполнения. При дальнейших изменениях хэш-таблицы ее карта размеров поддерживается в актуальном состоянии, поэтому последующие преобразования вызовами `par` и `seq` имеют постоянную сложность. Впрочем, поддержку карты размеров можно и отключить, используя метод `useSizeMap` хэш-таблицы. Важный момент: изменения, сделанные в последовательной хэш-таблице, видны в параллельной, и наоборот. + +## Параллельные префиксные хэш-деревья (Hash Tries) + +Параллельные префиксные хэш-деревья являются параллельным аналогом неизменяемых префиксных хэш-деревьев, которые используются для эффективного представления неизменяемых множеств и ассоциативных массивов. Последние представлены классами [immutable.ParHashSet](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/parallel/immutable/ParHashSet.html) и [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 + +[Компоновщики]({{ site.baseurl }}/overviews/parallel-collections/architecture.html#core_abstractions) параллельных хэш-деревьев действуют аналогично компоновщикам хэш-таблиц, а именно предварительно распределяют элементы по блокам, а после этого параллельно составляют результирующее хэш-дерево, назначая обработку различных блоков разным процессорам, каждый из которых независимо собирает свое поддерево. + +Параллельные хэш-деревья могут за постоянное время преобразовываться вызовами методов `seq` и `par` в последовательные хэш-деревья и обратно. + +## Параллельные многопоточные префиксные деревья (Concurrent Tries) + +Параллельным аналогом коллекции [concurrent.TrieMap](http://www.scala-lang.org/api/{{ site.scala-version }}/scala/collection/concurrent/TrieMap.html), представляющей собой многопоточный и потокозащищеный ассоциативный массив, является коллекция [mutable.ParTrieMap](http://www.scala-lang.org/api/{{ site.scala-version}}/scala/collection/parallel/mutable/ParTrieMap.html). В то время, как большинство многопоточных структур данных не гарантируют правильного перебора элементов в случае, если эта структура данных была изменена во время ее прохождения, многопоточные деревья `Ctries` гарантируют, что обновленные данные станут видны только при следующем прохождении. Это означает, что можно изменять многопоточное дерево прямо во время прохождения, как в следующем примере, в котором выводятся квадратные корни от 1 до 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) + ... + +[Компоновщики]({{ site.baseurl }}/ru/overviews/parallel-collections/architecture.html#core_abstractions) реализованы как `TrieMap`-- так как эта структура является многопоточной, при вызове метода трансформации создается только один компоновщик, разделяемый всеми процессорами. + +Как и в случае с другими параллельными изменяемыми коллекциями, экземпляры `TrieMap` и параллельных `ParTrieMap`, полученные вызовом методов `seq` или `par`, хранят данные в одном и том же хранилище, поэтому модификации одной коллекции видны в другой. Такие преобразования занимают постоянное время. + +## Характеристики производительности + +Характеристики производительности последовательных типов (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 | - | - | - | - | + +Характеристики производительности множеств (set) и ассоциативных массивов (map): + +| | lookup | add | remove | +| -------- | ---- | ---- | ---- | +| **неизменяемые** | | | | +| `ParHashSet`/`ParHashMap`| eC | eC | eC | +| **изменяемые** | | | | +| `ParHashSet`/`ParHashMap`| C | C | C | +| `ParTrieMap` | eC | eC | eC | + + +### Расшифровка + +Обозначения в двух представленных выше таблицах означают следующее: + +| | | +| --- | ---- | +| **C** | Операция (быстрая) выполняется за постоянное время. | +| **eC** | Операция выполняется за фактически постоянное время, но только при соблюдении некоторых предположений, например о максимальной длине вектора или распределении хэш-кодов.| +| **aC** | Операция выполняется за амортизированное постоянное время. Некоторые вызовы операции могут выполняться медленнее, но при подсчете времени выполнения большого количества операций выходит, что в среднем на операцию требуется постоянное время. | +| **Log** | Операция занимает время, пропорциональное логарифму размера коллекции. | +| **L** | Операция линейна, то есть занимает время, пропорциональное размеру коллекции. | +| **-** | Операция не поддерживается. | + +Первая таблица трактует последовательные типы-- изменяемые и неизменяемые-- в контексте выполнения следующих операций: + +| | | +| --- | ---- | +| **head** | Получение первого элемента последовательности. | +| **tail** | Получение новой последовательности, состоящей из всех элементов исходной, кроме первого. | +| **apply** | Индексирование. | +| **update** | Функциональное обновление (с помощью `updated`) для неизменяемых последовательностей, обновление с побочными действиями (с помощью `update`) для изменяемых. | +| **prepend**| Добавление элемента в начало последовательности. Для неизменяемых последовательностей создается новая последовательность, для изменяемых-- модифицируется существующая. | +| **append** | Добавление элемента в конец последовательности. Для неизменяемых последовательностей создается новая последовательность, для изменяемых-- модифицируется существующая. | +| **insert** | Вставка элемента в выбранную позицию последовательности. Поддерживается только изменяемыми последовательностями. | + +Вторая таблица рассматривает изменяемые и неизменяемые множества и ассоциативные массивы в контексте следующих операций: + +| | | +| --- | ---- | +| **lookup** | Проверка принадлежности элемента множеству, или получение значения, ассоциированного с ключом. | +| **add** | Добавление нового элемента во множество или новой пары ключ/значение в ассоциативный массив. | +| **remove** | Удаление элемента из множества или ключа из ассоциативного массива. | +| **min** | Минимальный элемент множества или минимальный ключ ассоциативного массива. | + diff --git a/ru/overviews/parallel-collections/custom-parallel-collections.md b/ru/overviews/parallel-collections/custom-parallel-collections.md new file mode 100644 index 0000000000..5d5e44e4f8 --- /dev/null +++ b/ru/overviews/parallel-collections/custom-parallel-collections.md @@ -0,0 +1,195 @@ +--- +layout: overview-large +title: Создание пользовательской параллельной коллекции + +disqus: true + +partof: parallel-collections +language: ru +num: 6 +--- + +## Параллельная коллекция без компоновщиков + +Определить параллельную коллекцию без определения ее компоновщика возможно, так же, как возможно определить собственную последовательную коллекцию без определения ее строителей (`builders`). Вследствие отсутствия компоновщика получится, что методы трансформаций (т.е. `map`, `flatMap`, `collect`, `filter`, ...) по умолчанию будут возвращать ближайшую по иерархии стандартную коллекцию. Например, диапазоны строителей не имеют, и поэтому создание отображения элементов диапазона-- `map` -- создает вектор. + +В следующем примере определим параллельную коллекцию-строку. Так как строки по сути являются неизменяемыми последовательностями, сделаем их класс наследником `immutable.ParSeq[Char]`: + + class ParString(val str: String) + extends immutable.ParSeq[Char] { + +Затем определим методы, которые есть в любой неизменяемой последовательности: + + def apply(i: Int) = str.charAt(i) + + def length = str.length + +Кроме того, мы должны решить, что будем возвращать в качестве последовательного аналога нашей параллельной коллекции. Пусть это будет класс `WrappedString`: + + def seq = new collection.immutable.WrappedString(str) + +И наконец, требуется задать разделитель для наших параллельных строк. Назовем его `ParStringSplitter` и сделаем его потомком разделителя последовательностей, то есть типа `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 + } + +В примере выше, `ntl` отображает общую длину строки, `i`-- текущую позицию, и наконец `s`-- саму строку. + +Итераторы (или разделители) параллельных коллекций требуют еще несколько методов помимо `next` и `hasNext`, характерных для итераторов последовательных коллекций. Для начала, у них есть метод `remaining`, возвращающий количество элементов, которые данному разделителю еще предстоит перебрать. Затем, метод `dup`, дублирующий текущий разделитель. + + def remaining = ntl - i + + def dup = new ParStringSplitter(s, i, ntl) + +И наконец, методы `split` и `psplit`, которые используются для создания разделителей, перебирающих подмножества элементов текущего разделителя. Для метода `split` действует соглашение, что он возвращает последовательность разделителей, перебирающих непересекающиеся подмножества элементов текущего разделителя, ни одно из которых не является пустым. Если текущий разделитель содержит один или менее элементов, `split` возвращает саму последовательность этого разделителя. Метод `psplit` должен возвращать последовательность разделителей, перебирающих точно такое количество элементов, которое задано значениями размеров, указанных параметром `sizes`. Если параметр `sizes` требует отделить меньше элементов, чем содержит текущий разделитель, то дополнительный разделитель со всеми остальными элементами размещается в конце последовательности. Если в параметре `sizes` указано больше элементов, чем содержится в текущем разделителе, для каждого размера, на который не хватило элементов, будет добавлен пустой разделитель. Наконец, вызов `split` или `psplit` делает текущий разделитель недействительным. + + 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 + } + } + } + +Выше приведена реализация метода `split` посредством вызова `psplit`, что часто наиболее оправдано в случае параллельных коллекций. Написать реализацию разделителя для параллельных ассоциативных массивов, множеств или итерируемых объектов чаще всего проще, так как они не требуют реализации метода `psplit`. + +Итак, мы получили класс параллельных строк. Единственным недостатком является то, что вызов методов трансформации, таких, как `filter`, произведет параллельный вектор вместо параллельной строки, что в ряде случаев может оказаться не самым оптимальным решением, так как воссоздание строки из вектора после фильтрации может оказаться затратным. + +## Параллельные коллекции с компоновщиками + +Допустим, мы хотим применить `filter` к символам параллельной строки, например, чтобы избавиться от запятых. Как отмечено выше, вызов `filter` вернет параллельный вектор, в то время как мы хотим получить строку (так как некоторые интерфейсы используемого API могут требовать последовательную строку). + +Чтобы избежать этого, для параллельной строки требуется написать компоновщик. На этот раз мы унаследуем трейт `ParSeqLike`, чтобы конкретизировать значение, возвращаемое методом `filter`-- а именно `ParString` вместо `ParSeq[Char]`. Третий параметр-тип трейта `ParSeqLike` указывает тип последовательного аналога параллельной коллекции (в этом отличие от последовательных трейтов вида `*Like`, имеющих только два параметра). + + class ParString(val str: String) + extends immutable.ParSeq[Char] + with ParSeqLike[Char, ParString, collection.immutable.WrappedString] + +Все методы остаются такими же, как в предыдущем примере, только дополнительно добавляется защищенный метод `newCombiner`, который используется при выполнении метода `filter`. + + protected[this] override def newCombiner: Combiner[Char, ParString] = new ParStringCombiner + +Следующим шагом определяем класс `ParStringCombiner`. Компоновщики являются подтипами строителей, в которых появляется дополнительный метод `combine`, принимающий другой компоновщик как аргумент и возвращающий новый компоновщик, который содержит элементы и текущего и принятого компоновщика. И текущий компоновщик, и компоновщик-аргумент становятся недействительными после вызова `combine`. Если передать аргументом сам текущий компоновщик, метод `combine` просто вернет его же как результат. Предполагается, что метод должен быть эффективным, то есть в худшем случае требовать для выполнения логарифмического времени по отношению к количеству элементов, так как в ходе параллельного вычисления он вызывается большое количество раз. + +Наш `ParStringCombiner` будет содержать последовательность строителей строк. Он будет реализовывать `+=` путем добавления элемента к последнему строителю строки в последовательности, и `combine` конкатенацией списков строителей строк текущего компоновщика и компоновщика-аргумента. Метод `result`, вызываемый в конце параллельного вычисления, произведет параллельную строку соединив все строители строк вместе. Таким образом, элементы копируются только один раз в конце, а не каждый раз, когда вызывается метод `combine`. В идеале, мы должны подумать о том, чтобы еще и копирование проводить параллельно (именно так и происходит в случае параллельных массивов), но без погружения в детали внутреннего представления строк это лучшее, чего мы можем добиться-- остается смириться с этим последовательным узким местом. + + 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 + } + } + + +## Как мне реализовать собственный компоновщик? В общих чертах? + +Тут нет стандартного рецепта, -- все зависит от имеющейся структуры данных, и обычно требует изобретательности со стороны того, кто пишет реализацию. Тем не менее, можно выделить несколько подходов, которые обычно применяются: + +1. Конкатенация и объединение. Некоторые структуры данных позволяют реализовать эти операции эффективно (обычно с логарифмической сложностью), и если требуемая коллекция представлена такой структурой данных, ее компоновщик может быть самой такой коллекцией. Особенно хорошо этот подход работает для пальчиковых деревьев (finger trees), веревочных деревьев (ropes) и различных видов куч. + +2. Двухфазное выполнение. Подход, применяемый в случае параллельных массивов и параллельных хэш-таблиц; он предполагает, что элементы могут быть эффективно рассортированы по готовым для конкатенации блокам, из которых результирующая структура данных может быть построена параллельно. В первую фазу блоки заполняются независимо различными процессорами, и в конце просто соединяются конкатенацией. Во вторую фазу происходит выделение памяти для целевой структуры данных, и после этого различные процессоры заполняют различные ее части, используя элементы непересекающихся блоков. +Следует принять меры для того, чтобы различные процессоры никогда не изменяли одну и ту же часть структуры данных, иначе не избежать трудноуловимых, связанных с многопоточностью ошибок. Такой подход легко применить к последовательностям с произвольным доступом, как было показано в предыдущем разделе. + +3. Многопоточная структура данных. Так как последние два подхода, в сущности, не требуют использования примитивных механизмов синхронизации, предполагается, что структура будет строиться несколькими потоками так, что два различных процессора никогда не будут изменять одну и ту же область памяти. Существует большое количество многопоточных структур данных, которые могут безопасно изменяться несколькими процессорами одновременно, среди таких можно упомянуть многопоточные списки с пропусками (skip lists), многопоточные хэш-таблицы, `split-ordered` списки и многопоточные АВЛ-деревья. +При этом требуется следить, чтобы у выбранной многопоточной структуры был горизонтально масштабируемый метод вставки. У многопоточных параллельных коллекций компоновщик может быть представлен самой коллекцией, и единственный его экземпляр обычно используется всеми процессорами, занятыми в выполнении параллельной операции. + +## Интеграция с фреймворком коллекций + +Наш класс `ParString` оказался не вполне завершен: несмотря на то, что мы реализовали собственный компоновщик, который будут использовать такие методы, как `filter`, `partition`, `takeWhile` или `span`, большинство методов трансформации требуют скрытый параметр-доказательство `CanBuildFrom` (подробное объяснение можно посмотреть в "Scala collections guide" (прим. перев. скорее {{ site.baseurl }}/overviews/core/architecture-of-scala-collections.html)). Чтобы обеспечить его доступность и тем самым полностью интегрировать наш класс `ParString` с фреймворком коллекций, требуется примешать дополнительный трейт `GenericParTemplate` и определить объект-компаньон для `ParString`. + + class ParString(val str: String) + extends immutable.ParSeq[Char] + with GenericParTemplate[Char, ParString] + with ParSeqLike[Char, ParString, collection.immutable.WrappedString] { + + def companion = ParString + +Внутрь объекта-компаньона помещаем скрытый параметр-доказательство `CanBuildFrom`. + + 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 + } + } + +## Дальнейшие настройки-- многопоточные и другие коллекции + +Процесс реализации многопоточной коллекции (в отличие от параллельных, многопоточные коллекции могут подобно `collection.concurrent.TrieMap` изменяться одновременно несколькими потоками) не всегда прост и очевиден. При этом особенно нуждаются в тщательном обдумывании компоновщики. Компоновщики большинства _параллельных_ коллекций, которые были рассмотрены до этого момента, используют двухфазное выполнение. На первом этапе элементы добавляются различными процессорами к своим компоновщикам и последние объединяются вместе. На втором шаге, когда становятся доступными все элементы, строится результирующая коллекция. + +Другим подходом является построение результирующей коллекции как структуры элементов компоновщика. Для этого коллекция должна быть потокозащищенной-- компоновщик должен позволять выполнить _многопоточную_ вставку элемента. В этом случае один компоновщик может использоваться всеми процессорами. + +Если требуется сделать многопоточную коллекцию параллельной, в ее компоновщике нужно перегрузить метод `canBeShared` так, чтобы он возвращал `true`. Этим мы заставим проверять, что при выполнении параллельной операции создается только один компоновщик. Далее, метод `+=` должен быть потокозащищенным. И наконец, метод `combine` по-прежнему должен возвращать текущий компоновщик в случае, если он совпадает с аргументом, а в противном случае вполне может выбросить исключение. + +Чтобы добиться лучшей балансировки нагрузки, разделители делятся на более мелкие разделители. По умолчанию решение о том, что дальнейшее разделение не требуется, принимается на основе информации, возвращенной методом `remaining`. Для некоторых коллекций вызов метода `remaining` может быть затратным, и решение о разделении лучше принять другими способами. В этом случае нужно перегрузить метод `shouldSplitFurther` разделителя. + +В реализации по умолчанию разделитель делится, если число оставшихся элементов больше, чем размер коллекции деленный на взятый восемь раз уровень параллелизма. + + def shouldSplitFurther[S](coll: ParIterable[S], parallelismLevel: Int) = + remaining > thresholdFromSize(coll.size, parallelismLevel) + +Как вариант, разделитель может иметь счетчик количества проведенных над ним разделений и реализовывать метод `shouldSplitFurther`, возвращая `true`, если количество разделений больше, чем `3 + log(parallelismLevel)`. Это и позволяет избежать вызова метода `remaining`. + +Более того, если для некоторой коллекции вызов `remaining` затратен (то есть требует обработки большого числа элементов), то метод `isRemainingCheap` в разделителях следует перегрузить, так, чтобы он возвращал `false`. + +Наконец, если реализовать метод `remaining` в разделителях весьма затруднительно, можно возвращать `false` в перегруженном методе `isStrictSplitterCollection` соответствующей коллекции. Над такими коллекциями не получится выполнить ряд методов, в частности таких, которые требуют точности разделителей (последнее предполагает как раз, что метод `remaining` возвращает правильное значение). Но, что важно, это не относится к методам, используемым для обработки for-включений (for-comprehensions). From a00cedc8d19f92e3858c813f27675fe3baec3e13 Mon Sep 17 00:00:00 2001 From: ReturnedToLife Date: Tue, 21 Mar 2017 11:36:39 +0300 Subject: [PATCH 06/12] a most difficult subpart --- .../parallel-collections/configuration.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 ru/overviews/parallel-collections/configuration.md diff --git a/ru/overviews/parallel-collections/configuration.md b/ru/overviews/parallel-collections/configuration.md new file mode 100644 index 0000000000..7a76a15ae3 --- /dev/null +++ b/ru/overviews/parallel-collections/configuration.md @@ -0,0 +1,58 @@ +--- +layout: overview-large +title: + +disqus: true + +partof: parallel-collections +language: ru +num: 7 +--- + +## + + . , . + + ; , . , , \[[1][1]\]. + + . , `ForkJoinTaskSupport` "fork-join" JVM 1.6 . `ThreadPoolTaskSupport` JVM 1.5 , "fork-join". `ExecutionContextTaskSupport` `scala.concurrent`, , `scala.concurrent` ( JVM, "fork-join" "thread pool executor"). , "fork-join", API "future". + + : + + 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) + + "fork-join" 2. "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) + + , . , -- . + + , `TaskSupport` : + + def execute[R, Tp](task: Task[R, Tp]): () => R + + def executeAndWaitResult[R, Tp](task: Task[R, Tp]): R + + def parallelismLevel: Int + + `execute` "future" . `executeAndWait` , . `parallelismLevel` , . + +## + +1. [On a Generic Parallel Collection Framework, June 2011][1] + + [1]: http://infoscience.epfl.ch/record/165523/files/techrep.pdf "parallel-collections" From 1fb53bd04046b7054adaf8d3b6ce803544f8614a Mon Sep 17 00:00:00 2001 From: ReturnedToLife Date: Tue, 21 Mar 2017 12:05:29 +0300 Subject: [PATCH 07/12] last one subpart --- .../parallel-collections/performance.md | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 ru/overviews/parallel-collections/performance.md diff --git a/ru/overviews/parallel-collections/performance.md b/ru/overviews/parallel-collections/performance.md new file mode 100644 index 0000000000..0399cdb4f2 --- /dev/null +++ b/ru/overviews/parallel-collections/performance.md @@ -0,0 +1,191 @@ +--- +layout: overview-large +title: + +disqus: true + +partof: parallel-collections +num: 8 +outof: 8 +language: ru +--- + +## JVM + + JVM , -- , , . . + + , JVM , ( \[[2][2]\]). Java Scala JVM. Java- , . " " JIT- (JIT just-in-time). - , " " , . , , HotSpot , . , , , , : ( , ) JVM , . , , JIT-, . + + , JVM . . - ( JVM ), , , . , , . + + (boxing unboxing), , (generic) . , . , , , . + + (memory contention), - , , . , - , , , . . + +## + + , . , JIT- ( ), . (warm-up). + + , , , " ", JVM. + + , HotSpot JVM, . + +, , , , , . + + Scala `scala.testing.Benchmark`, . `map` : + + 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 + } + } + } + + `run` , . `Map`, `scala.testing.Benchmark`, `par` `length`. + + , , : + + java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=300000 Map 10 + + `server` VM. `cp` "classpath", , jar- Scala. `-Dpar` `-Dlength`-- . , `10` JVM 10 . + + `par` `1`, `2`, `4` `8`, + i7 : + + 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 + + , , . , , , `4` `8` . + +## ? + + , . + + , , . ( ) : + +- . CPU . , , , . +- JVM. . `ForkJoinPool`, `ThreadPoolExecutor`, . +- . , , , . , . +- . , `ParArray` `ParTrieMap` , . +- . , `ParVector` (, `filter`) ( `foreach`) +- . `foreach`, `map`, , . +- . . , . + + , - , , . , , ( -- ) i7 ( ) 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 + } + } + } + + `250000` `1`, `2` `4` : + + 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 + + `120000` `4` : + + 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` . + + `map` ( ) `mutable.ParHashMap` : + + 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 + } + } + } + + `120000` `1` `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 + + `15000` -: + + 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 + + `15000` ( - - , ). + +## + +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 411f54dddb6584e93e3228063e6f3ba99e0d60f9 Mon Sep 17 00:00:00 2001 From: ReturnedToLife Date: Tue, 21 Mar 2017 12:09:12 +0300 Subject: [PATCH 08/12] links small edit --- ru/overviews/parallel-collections/overview.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ru/overviews/parallel-collections/overview.md b/ru/overviews/parallel-collections/overview.md index e4c8da15f4..5e04b1130a 100644 --- a/ru/overviews/parallel-collections/overview.md +++ b/ru/overviews/parallel-collections/overview.md @@ -48,7 +48,7 @@ language: ru Попробуем изобразить всеобщность и полезность представленных коллекций на ряде простых примеров, для каждого из которых характерно прозрачно-параллельное выполнение. -_Примечание:_ Некоторые из последующих примеров оперируют небольшими коллекциями, для которых такой подход не рекомендуется. Они должны рассматриваться только как иллюстрация. Эвристически, ускорение становится заметным, когда размер коллекции дорастает до нескольких тысяч элементов. (Более подробно о взаимосвязи между размером коллекции и производительностью, смотрите [соответствующий подраздел]({{ site.baseurl}}/overviews/parallel-collections/performance.html#how_big_should_a_collection_be_to_go_parallel) раздела, посвященного [производительности]({{ site.baseurl }}/overviews/parallel-collections/performance.html) в данном руководстве.) +_Примечание:_ Некоторые из последующих примеров оперируют небольшими коллекциями, для которых такой подход не рекомендуется. Они должны рассматриваться только как иллюстрация. Эвристически, ускорение становится заметным, когда размер коллекции дорастает до нескольких тысяч элементов. (Более подробно о взаимосвязи между размером коллекции и производительностью, смотрите [соответствующий подраздел]({{ site.baseurl}}/overviews/parallel-collections/performance.html#how_big_should_a_collection_be_to_go_parallel) раздела, посвященного [производительности]({{ site.baseurl }}/ru/overviews/parallel-collections/performance.html) в данном руководстве.) #### map @@ -99,7 +99,7 @@ _Примечание:_ Некоторые из последующих прим _На заметку:_ Коллекции, являющиеся последовательными в силу наследования (в том смысле, что доступ к их элементам требуется получать по порядку, один элемент за другим), такие, как списки, очереди и потоки (streams), преобразовываются в свои параллельные аналоги копированием элементов в соответствующие параллельные коллекции. Например, список `List` конвертируется в стандартную неизменяемую параллельную последовательность, то есть в `ParVector`. Естественно, что копирование, которое для этого требуется, вносит дополнительный расход производительности, которого не требуют другие типы коллекций, такие как `Array`, `Vector`, `HashMap` и т.д. -Больше информации о конвертировании можно найти в разделах [преобразования]({{ site.baseurl }}/overviews/parallel-collections/conversions.html) и [конкретные классы параллельных коллекций]({{ site.baseurl }}/overviews/parallel-collections/concrete-parallel-collections.html) этого руководства. +Больше информации о конвертировании можно найти в разделах [преобразования]({{ site.baseurl }}/ru/overviews/parallel-collections/conversions.html) и [конкретные классы параллельных коллекций]({{ site.baseurl }}/ru/overviews/parallel-collections/concrete-parallel-collections.html) этого руководства. ## Семантика @@ -176,5 +176,5 @@ _Примечание:_ Часто возникает мысль, что так _"Неупорядоченная"_ семантика параллельных коллекций означает только то, что операции будут выполнены не по порядку (во _временном_ отношении. То есть, не последовательно), она не означает, что результат будет "*перемешан*" относительно изначального порядка (в _пространственном_ отношении). Напротив, результат будет практически всегда пересобран _по-порядку_-- то есть, параллельная коллекция, разбитая на части в порядке A, B, C, будет снова объединена в том же порядке A, B, C, а не в каком-то произвольном, например, B, C, A. -Если требуется больше информации о том, как разделяются и комбинируются операции на различных типах коллекций, посетите раздел [Архитектура]({{ site.baseurl }}/overviews/parallel-collections/architecture.html) этого руководства. +Если требуется больше информации о том, как разделяются и комбинируются операции на различных типах коллекций, посетите раздел [Архитектура]({{ site.baseurl }}/ru/overviews/parallel-collections/architecture.html) этого руководства. From 5438f9243f91d5da23800a91383ee935c2aa8ec9 Mon Sep 17 00:00:00 2001 From: ReturnedToLife Date: Wed, 22 Mar 2017 15:22:17 +0300 Subject: [PATCH 09/12] links to ru and small style fix --- overviews/parallel-collections/architecture.md | 2 +- .../parallel-collections/concrete-parallel-collections.md | 2 +- overviews/parallel-collections/configuration.md | 2 +- overviews/parallel-collections/conversions.md | 2 +- overviews/parallel-collections/ctries.md | 2 +- overviews/parallel-collections/custom-parallel-collections.md | 2 +- overviews/parallel-collections/performance.md | 2 +- ru/overviews/parallel-collections/architecture.md | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/overviews/parallel-collections/architecture.md b/overviews/parallel-collections/architecture.md index 85de2b052a..d99c169cd5 100644 --- a/overviews/parallel-collections/architecture.md +++ b/overviews/parallel-collections/architecture.md @@ -5,7 +5,7 @@ title: Architecture of the Parallel Collections Library disqus: true partof: parallel-collections -languages: [ja, zh-cn, es] +languages: [ja, zh-cn, es, ru] num: 5 --- diff --git a/overviews/parallel-collections/concrete-parallel-collections.md b/overviews/parallel-collections/concrete-parallel-collections.md index 12bbea3b0d..99d6810632 100644 --- a/overviews/parallel-collections/concrete-parallel-collections.md +++ b/overviews/parallel-collections/concrete-parallel-collections.md @@ -5,7 +5,7 @@ title: Concrete Parallel Collection Classes disqus: true partof: parallel-collections -languages: [ja, zh-cn, es] +languages: [ja, zh-cn, es, ru] num: 2 --- diff --git a/overviews/parallel-collections/configuration.md b/overviews/parallel-collections/configuration.md index 2570d8bcda..704546de78 100644 --- a/overviews/parallel-collections/configuration.md +++ b/overviews/parallel-collections/configuration.md @@ -5,7 +5,7 @@ title: Configuring Parallel Collections disqus: true partof: parallel-collections -languages: [ja, zh-cn, es] +languages: [ja, zh-cn, es, ru] num: 7 --- diff --git a/overviews/parallel-collections/conversions.md b/overviews/parallel-collections/conversions.md index 5b1303355f..329490491f 100644 --- a/overviews/parallel-collections/conversions.md +++ b/overviews/parallel-collections/conversions.md @@ -5,7 +5,7 @@ title: Parallel Collection Conversions disqus: true partof: parallel-collections -languages: [ja, zh-cn, es] +languages: [ja, zh-cn, es, ru] num: 3 --- diff --git a/overviews/parallel-collections/ctries.md b/overviews/parallel-collections/ctries.md index 46df557d35..ca839709ac 100644 --- a/overviews/parallel-collections/ctries.md +++ b/overviews/parallel-collections/ctries.md @@ -5,7 +5,7 @@ title: Concurrent Tries disqus: true partof: parallel-collections -languages: [ja, zh-cn, es] +languages: [ja, zh-cn, es, ru] num: 4 --- diff --git a/overviews/parallel-collections/custom-parallel-collections.md b/overviews/parallel-collections/custom-parallel-collections.md index 528b5a8c3f..c6067b2862 100644 --- a/overviews/parallel-collections/custom-parallel-collections.md +++ b/overviews/parallel-collections/custom-parallel-collections.md @@ -5,7 +5,7 @@ title: Creating Custom Parallel Collections disqus: true partof: parallel-collections -languages: [ja, zh-cn, es] +languages: [ja, zh-cn, es, ru] num: 6 --- diff --git a/overviews/parallel-collections/performance.md b/overviews/parallel-collections/performance.md index abfb4c0e0b..daadbcbb04 100644 --- a/overviews/parallel-collections/performance.md +++ b/overviews/parallel-collections/performance.md @@ -7,7 +7,7 @@ disqus: true partof: parallel-collections num: 8 outof: 8 -languages: [ja, zh-cn, es] +languages: [ja, zh-cn, es, ru] --- ## Performance on the JVM diff --git a/ru/overviews/parallel-collections/architecture.md b/ru/overviews/parallel-collections/architecture.md index e24fabb2df..d55ab9d4ec 100644 --- a/ru/overviews/parallel-collections/architecture.md +++ b/ru/overviews/parallel-collections/architecture.md @@ -1,4 +1,4 @@ ---- +--- layout: overview-large title: Архитектура библиотеки параллельных коллекций @@ -9,7 +9,7 @@ language: ru num: 5 --- -Так же, как и обычная последовательная, параллельная библиотека коллекций Scala существует во множестве различных реализаций достаточно большого количества операций над коллекциями. И так же, как последовательная, библиотека избегает повторений кода путем реализации большинства операций посредством собственных "шаблонов", которые достаточно объявить один раз, а потом гибко наследовать в различных реализациях параллельных коллекций. +Так же, как и обычная библиотека коллекций Scala, библиотека параллельных коллекций содержит большое количество операций, для которых, в свою очередь, существует множество различных реализаций. И так же, как последовательная, параллельная библиотека избегает повторений кода путем реализации большинства операций посредством собственных "шаблонов", которые достаточно объявить один раз, а потом гибко наследовать в различных реализациях параллельных коллекций. Преимущества этого подхода сильно облегчают **поддержку** и **расширяемость**. Поддержка может стать простой и надежной, когда одна реализация операции над параллельной коллекцией наследуется всеми параллельными коллекциями; исправления ошибок в этом случае сами распространяются вниз по иерархии классов, а не требуют дублировать реализации. По тем же причинам всю библиотеку становится проще расширять-- новые классы коллекций могут просто унаследовать большинство имеющихся операций. From cf615aeeecf1aba5510f6ffff3d917b225380f33 Mon Sep 17 00:00:00 2001 From: ReturnedToLife Date: Wed, 22 Mar 2017 17:19:09 +0300 Subject: [PATCH 10/12] encoding fix --- .../parallel-collections/configuration.md | 24 +++--- .../parallel-collections/performance.md | 78 +++++++++---------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/ru/overviews/parallel-collections/configuration.md b/ru/overviews/parallel-collections/configuration.md index 7a76a15ae3..261a8b31a3 100644 --- a/ru/overviews/parallel-collections/configuration.md +++ b/ru/overviews/parallel-collections/configuration.md @@ -1,6 +1,6 @@ ---- +--- layout: overview-large -title: +title: Конфигурирование параллельных коллекций disqus: true @@ -9,15 +9,15 @@ language: ru num: 7 --- -## +## Обслуживание задач - . , . +Параллельные коллекции предоставляют возможность выбора методов планирования задач при выполнении операций. В числе параметров каждой параллельной коллекции есть так называемый объект обслуживания задач, который и отвечает за планирование и распределение нагрузки на процессоры. - ; , . , , \[[1][1]\]. +Внутри объект обслуживания задач содержит ссылку на реализацию пула потоков; кроме того он определяет, как и когда задачи разбиваются на более мелкие подзадачи. Подробнее о том, как конкретно происходит этот процесс, можно узнать в техническом отчете \[[1][1]\]. - . , `ForkJoinTaskSupport` "fork-join" JVM 1.6 . `ThreadPoolTaskSupport` JVM 1.5 , "fork-join". `ExecutionContextTaskSupport` `scala.concurrent`, , `scala.concurrent` ( JVM, "fork-join" "thread pool executor"). , "fork-join", API "future". +В настоящее время для параллельных коллекций доступно несколько реализаций объекта поддержки задач. Например, `ForkJoinTaskSupport` реализован посредством "fork-join" пула и используется по умолчанию на JVM 1.6 или более поздних. Менее эффективный `ThreadPoolTaskSupport` является резервом для JVM 1.5 и тех машин, которые не поддерживают пулы "fork-join". `ExecutionContextTaskSupport` берет реализацию контекста исполнения по умолчанию из `scala.concurrent`, и использует тот же пул потоков, что и `scala.concurrent` (в зависимости от версии JVM, это может быть пул "fork-join" или "thread pool executor"). По умолчанию каждой параллельной коллекции назначается именно обслуживание задач контекста выполнения, поэтому параллельные коллекции используют тот же пул "fork-join", что и API объектов "future". - : +Сменить метод обслуживания задач для параллельной коллекции можно так: scala> import scala.collection.parallel._ import scala.collection.parallel._ @@ -31,7 +31,7 @@ num: 7 scala> pc map { _ + 1 } res0: scala.collection.parallel.mutable.ParArray[Int] = ParArray(2, 3, 4) - "fork-join" 2. "thread pool executor" : +Приведенное выше настраивает параллельную коллекцию на использование "fork-join" пула с уровнем параллелизма равным 2. Заставить коллекцию использовать "thread pool executor" можно так: scala> pc.tasksupport = new ThreadPoolTaskSupport() pc.tasksupport: scala.collection.parallel.TaskSupport = scala.collection.parallel.ThreadPoolTaskSupport@1d914a39 @@ -39,9 +39,9 @@ num: 7 scala> pc map { _ + 1 } res1: scala.collection.parallel.mutable.ParArray[Int] = ParArray(2, 3, 4) - , . , -- . +Когда параллельная коллекция сериализуется, поле объекта обслуживания задач исключается из сериализуемых. Когда параллельная коллекция восстанавливается из полученной последовательности байт, это поле приобретает значение по умолчанию-- способ обслуживания задач контекста выполнения. - , `TaskSupport` : +Чтобы реализовать собственный механизм поддержки задач, достаточно расширить трейт `TaskSupport` и реализовать следующие методы: def execute[R, Tp](task: Task[R, Tp]): () => R @@ -49,9 +49,9 @@ num: 7 def parallelismLevel: Int - `execute` "future" . `executeAndWait` , . `parallelismLevel` , . +Метод `execute` планирует асинхронное выполнение задачи и возвращает "future" в качестве ссылки к будущему результату выполнения. Метод `executeAndWait` делает то же самое, но возвращает результат только после завершения задачи. Метод `parallelismLevel` просто возвращает предпочитаемое количество ядер, которые обслуживание задач использует для планирования заданий. -## +## Ссылки 1. [On a Generic Parallel Collection Framework, June 2011][1] diff --git a/ru/overviews/parallel-collections/performance.md b/ru/overviews/parallel-collections/performance.md index 0399cdb4f2..f6676c516c 100644 --- a/ru/overviews/parallel-collections/performance.md +++ b/ru/overviews/parallel-collections/performance.md @@ -1,6 +1,6 @@ ---- +--- layout: overview-large -title: +title: Измерение производительности disqus: true @@ -10,29 +10,29 @@ outof: 8 language: ru --- -## JVM +## Производительность JVM - JVM , -- , , . . +При описании модели производительности выполнения кода на JVM иногда ограничиваются несколькими комментариями, и как результат-- не всегда становится хорошо понятно, что в силу различных причин написанный код может быть не таким производительным или расширяемым, как можно было бы ожидать. В этой главе будут приведены несколько примеров. - , JVM , ( \[[2][2]\]). Java Scala JVM. Java- , . " " JIT- (JIT just-in-time). - , " " , . , , HotSpot , . , , , , : ( , ) JVM , . , , JIT-, . +Одной из причин является то, что процесс компиляции выполняющегося на JVM приложения не такой, как у языка со статической компиляцией (как можно увидеть здесь \[[2][2]\]). Компиляторы Java и Scala обходятся минимальной оптимизацией при преобразовании исходных текстов в байткод JVM. При первом запуске на большинстве современных виртуальных Java-машин байткод преобразуется в машинный код той архитектуры, на которой он запущен. Это преобразование называется компиляцией "на лету" или JIT-компиляцией (JIT от just-in-time). Однако из-за того, что компиляция "на лету" должна быть быстрой, уровень оптимизации при такой компиляции остается низким. Более того, чтобы избежать повторной компиляции, компилятор HotSpot оптимизирует только те участки кода, которые выполняются часто. Поэтому тот, кто пишет тест производительности, должен учитывать, что программа может показывать разную производительность каждый раз, когда ее запускают: многократное выполнение одного и того же куска кода (то есть, метода) на одном экземпляре JVM может демонстрировать очень разные результаты замеров производительности в зависимости от того, оптимизировался ли определенный код между запусками. Более того, измеренное время выполнения некоторого участка кода может включать в себя время, за которое произошла сама оптимизация JIT-компилятором, что сделает результат измерения нерепрезентативным. - , JVM . . - ( JVM ), , , . , , . +Кроме этого, результат может включать в себя потраченное на стороне JVM время на осуществление операций автоматического управления памятью. Время от времени выполнение программы прерывается и вызывается сборщик мусора. Если исследуемая программа размещает хоть какие-нибудь данные в куче (а большинство программ JVM размещают), значит сборщик мусора должен запуститься, возможно, исказив при этом результаты измерений. Можно нивелировать влияние сборщика мусора на результат, запустив измеряемую программу множество раз, и тем самым спровоцировав большое количество циклов сборки мусора. - (boxing unboxing), , (generic) . , . , , , . +Одной из распространенных причин ухудшения производительности является оборачивание и разворачивание (boxing и unboxing), неявно происходящее в случаях, когда примитивный тип передается аргументом в обобщенный (generic) метод. Чтобы примитивные типы можно было передать в метод с параметром обобщенного типа, они во время выполнения преобразуются в представляющие их объекты. Этот процесс замедляет выполнение, а кроме того порождает необходимость в дополнительном выделении памяти и, соответственно, создает дополнительный мусор в куче. - (memory contention), - , , . , - , , , . . +В качестве распространенной причины ухудшения параллельной производительности можно назвать соперничество за память (memory contention), возникающее из-за того, что программист не может явно указать, где следует размещать объекты. Фактически, из-за влияния сборщика мусора, это соперничество может произойти на более поздней стадии жизни приложения, а именно после того, как объекты начнут перемещаться в памяти. Такие влияния нужно учитывать при написании теста. -## +## Пример микротеста производительности - , . , JIT- ( ), . (warm-up). +Существует несколько подходов, позволяющих избежать описанных выше эффектов во время измерений. В первую очередь следует убедиться, что JIT-компилятор преобразовал исходный текст в машинный код (и что последний был оптимизирован), прогнав микротест производительности достаточное количество раз. Этот процесс известен как фаза разогрева (warm-up). - , , , " ", JVM. +Для того, чтобы уменьшить число помех, вызванных сборкой мусора от объектов, размещенных другими участками программы или несвязанной компиляцией "на лету", требуется запустить микротест на отдельном экземпляре JVM. - , HotSpot JVM, . +Кроме того, запуск следует производить на серверной версии HotSpot JVM, которая выполняет более агрессивную оптимизацию. -, , , , , . +Наконец, чтобы уменьшить вероятность того, что сборка мусора произойдет посреди микротеста, лучше всего добиться выполнения цикла сборки мусора перед началом теста, а следующий цикл отложить настолько, насколько это возможно. - Scala `scala.testing.Benchmark`, . `map` : +В стандартной библиотеке Scala предопределен трейт `scala.testing.Benchmark`, спроектированный с учетом приведенных выше соображений. Ниже приведен пример тестирования производительности операции `map` многопоточного префиксного дерева: import collection.parallel.mutable.ParTrieMap import collection.parallel.ForkJoinTaskSupport @@ -51,39 +51,39 @@ language: ru } } - `run` , . `Map`, `scala.testing.Benchmark`, `par` `length`. +Метод `run` содержит код микротеста, который будет повторно запускаться для измерения времени своего выполнения. Объект `Map`, расширяющий трейт `scala.testing.Benchmark`, запрашивает передаваемые системой параметры уровня параллелизма `par` и количества элементов дерева `length`. - , , : +После компиляции программу, приведенную выше, следует запустить так: java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=300000 Map 10 - `server` VM. `cp` "classpath", , jar- Scala. `-Dpar` `-Dlength`-- . , `10` JVM 10 . +Флаг `server` требует использовать серверную VM. Флаг `cp` означает "classpath", то есть указывает, что файлы классов требуется искать в текущем каталоге и в jar-архиве библиотеки Scala. Аргументы `-Dpar` и `-Dlength`-- это уровень параллелизма и количество элементов соответственно. Наконец, `10` означает что тест производительности будет запущен на одной и той же JVM именно 10 раз. - `par` `1`, `2`, `4` `8`, - i7 : +Устанавливая уровень параллелизма `par` в `1`, `2`, `4` и `8`, получаем следующее время выполнения +на четырехъядерном i7 с поддержкой гиперпоточности: 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 - , , . , , , `4` `8` . +Можно заметить, что на первые запуски требуется больше времени, но после оптимизации кода оно уменьшается. Кроме того, мы можем увидеть что гиперпотоковость не дает большого преимущества в нашем примере, это следует из того, что увеличение количества потоков от `4` до `8` не приводит к значительному увеличению производительности. -## ? +## Насколько большую коллекцию стоит сделать параллельной? - , . +Этот вопрос задается часто, но ответ на него достаточно запутан. - , , . ( ) : +Размер коллекции, при котором оправданы затраты на параллелизацию, в действительности зависит от многих факторов. Некоторые из них (но не все) приведены ниже: -- . CPU . , , , . -- JVM. . `ForkJoinPool`, `ThreadPoolExecutor`, . -- . , , , . , . -- . , `ParArray` `ParTrieMap` , . -- . , `ParVector` (, `filter`) ( `foreach`) -- . `foreach`, `map`, , . -- . . , . +- Архитектура системы. Различные типы CPU имеют различную архитектуру и различные характеристики масштабируемости. Помимо этого, машина может быть многоядерной, а может иметь несколько процессоров, взаимодействующих через материнскую плату. +- Производитель и версия JVM. Различные виртуальные машины применяют различные оптимизации кода во время выполнения и реализуют различные механизмы синхронизации и управления памятью. Некоторые из них не поддерживают `ForkJoinPool`, возвращая нас к использованию `ThreadPoolExecutor`, что приводит к увеличению накладных расходов. +- Поэлементная нагрузка. Величина нагрузки, оказываемой обработкой одного элемента, зависит от функции или предиката, которые требуется выполнить параллельно. Чем меньше эта нагрузка, тем выше должно быть количество элементов для получения ускорения производительности при параллельном выполнении. +- Выбранная коллекция. Например, разделители `ParArray` и `ParTrieMap` перебирают элементы коллекции с различными скоростями, а значит разницу количества нагрузки при обработке каждого элемента создает уже сам перебор. +- Выбранная операция. Например, у `ParVector` намного медленнее методы трансформации (такие, как `filter`) чем методы получения доступа (как `foreach`) +- Побочные эффекты. При изменении областей памяти несколькими потоками или при использовании механизмов синхронизации внутри тела `foreach`, `map`, и тому подобных, может возникнуть соперничество. +- Управление памятью. Размещение большого количества объектов может спровоцировать цикл сборки мусора. В зависимости от способа передачи ссылок на новые объекты, цикл сборки мусора может занимать больше или меньше времени. - , - , , . , , ( -- ) i7 ( ) JDK7: +Даже рассматривая вышеперечисленные факторы по отдельности, не так-то просто рассуждать о влиянии каждого, а тем более дать точный ответ, каким же должен быть размер коллекции. Чтобы в первом приближении проиллюстрировать, каким же он должен быть, приведем пример выполнения быстрой и не вызывающей побочных эффектов операции сокращения параллельного вектора (в нашем случае-- суммированием) на четырехъядерном процессоре i7 (без использования гиперпоточности) на JDK7: import collection.parallel.immutable.ParVector @@ -112,7 +112,7 @@ language: ru } } - `250000` `1`, `2` `4` : +Сначала запустим тест производительности с `250000` элементами и получим следующие результаты для `1`, `2` и `4` потоков: 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 @@ -121,16 +121,16 @@ language: ru 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 - `120000` `4` : +Затем уменьшим количество элементов до `120000` и будем использовать `4` потока для сравнения со временем сокращения последовательного вектора: 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` . +Похоже, что `120000` близко к пограничному значению в этом случае. - `map` ( ) `mutable.ParHashMap` : +В качестве еще одного примера возьмем метод `map` (метод трансформации) коллекции `mutable.ParHashMap` и запустим следующий тест производительности в той же среде: import collection.parallel.mutable.ParHashMap @@ -159,7 +159,7 @@ language: ru } } - `120000` `1` `4`: +Для `120000` элементов получаем следующие значения времени на количестве потоков от `1` до `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 @@ -168,7 +168,7 @@ language: ru 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 - `15000` -: +Теперь уменьшим число элементов до `15000` и сравним с последовательным хэш-отображением: 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 @@ -177,9 +177,9 @@ language: ru 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 - `15000` ( - - , ). +Для выбранных в этом случае коллекции и операции есть смысл сделать вычисление параллельным при количестве элементов больше `15000` (в общем случае хэш-отображения и хэш-множества возможно делать параллельными на меньших количествах элементов, чем требовалось бы для массивов или векторов). -## +## Ссылки 1. [Anatomy of a flawed microbenchmark, Brian Goetz][1] 2. [Dynamic compilation and performance measurement, Brian Goetz][2] From 53dcf46ac5ac773796bcf64677badb617ebe88e6 Mon Sep 17 00:00:00 2001 From: ReturnedToLife Date: Thu, 23 Mar 2017 15:46:23 +0300 Subject: [PATCH 11/12] bug and style fix --- ru/overviews/parallel-collections/architecture.md | 12 ++++++------ ru/overviews/parallel-collections/configuration.md | 8 ++++---- ru/overviews/parallel-collections/conversions.md | 4 ++-- ru/overviews/parallel-collections/ctries.md | 8 ++++---- .../custom-parallel-collections.md | 8 ++++---- ru/overviews/parallel-collections/performance.md | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/ru/overviews/parallel-collections/architecture.md b/ru/overviews/parallel-collections/architecture.md index d55ab9d4ec..86e0bff59a 100644 --- a/ru/overviews/parallel-collections/architecture.md +++ b/ru/overviews/parallel-collections/architecture.md @@ -9,9 +9,9 @@ language: ru num: 5 --- -Так же, как и обычная библиотека коллекций Scala, библиотека параллельных коллекций содержит большое количество операций, для которых, в свою очередь, существует множество различных реализаций. И так же, как последовательная, параллельная библиотека избегает повторений кода путем реализации большинства операций посредством собственных "шаблонов", которые достаточно объявить один раз, а потом гибко наследовать в различных реализациях параллельных коллекций. +Как и обычная библиотека коллекций Scala, библиотека параллельных коллекций содержит большое количество операций, для которых, в свою очередь, существует множество различных реализаций. И так же, как последовательная, параллельная библиотека избегает повторений кода путем реализации большинства операций посредством собственных "шаблонов", которые достаточно объявить один раз, а потом наследовать в различных реализациях параллельных коллекций. -Преимущества этого подхода сильно облегчают **поддержку** и **расширяемость**. Поддержка может стать простой и надежной, когда одна реализация операции над параллельной коллекцией наследуется всеми параллельными коллекциями; исправления ошибок в этом случае сами распространяются вниз по иерархии классов, а не требуют дублировать реализации. По тем же причинам всю библиотеку становится проще расширять-- новые классы коллекций могут просто унаследовать большинство имеющихся операций. +Преимущества этого подхода сильно облегчают **поддержку** и **расширяемость**. Поддержка станет простой и надежной, когда одна реализация операции над параллельной коллекцией унаследуется всеми параллельными коллекциями; исправления ошибок в этом случае сами распространятся вниз по иерархии классов, а не потребуют дублировать реализации. По тем же причинам всю библиотеку проще расширять-- новые классы коллекций наследуют большинство имеющихся операций. ## Ключевые абстракции @@ -20,21 +20,21 @@ num: 5 ### Разделители -Задача разделителя `Splitter`, как и предполагает имя, заключается в том, чтобы неочевидным образом разбить параллельную коллекцию на разделы. А основная идея-- в том, чтобы разбивать коллекцию на более мелкие части, пока их размер не станет подходящим для последовательной обработки. +Задача разделителя `Splitter`, как и предполагает имя, заключается в том, чтобы разбить параллельную коллекцию на непустые разделы. А основная идея-- в том, чтобы разбивать коллекцию на более мелкие части, пока их размер не станет подходящим для последовательной обработки. trait Splitter[T] extends Iterator[T] { def split: Seq[Splitter[T]] } -Что интересно, разделители `Splitter` реализованы через итераторы-- `Iterator`, а это подразумевает, что помимо разделения, они позволяют фреймворку перебирать элементы параллельной коллекции (то есть, наследуют стандартные методы трейта `Iterator`, такие, как `next` и `hasNext`.) Уникальность этого "разделяющего итератора" в том, что его метод `split` разбивает текущий объект `this` (мы помним, что `Splitter`, это подтип `Iterator`а) на другие разделители `Splitter`, каждый из которых перебирает свой, **отделенный** набор элементов когда-то целой параллельной коллекции. И так же, как любой нормальный `Iterator`, `Splitter` становится недействительным после того, как вызван его метод `split`. +Что интересно, разделители `Splitter` реализованы через итераторы-- `Iterator`, а это подразумевает, что помимо разделения, они позволяют перебирать элементы параллельной коллекции (то есть, наследуют стандартные методы трейта `Iterator`, такие, как `next` и `hasNext`.) Уникальность этого "разделяющего итератора" в том, что его метод `split` разбивает текущий объект `this` (мы помним, что `Splitter`, это подтип `Iterator`а) на другие разделители `Splitter`, каждый из которых перебирает свой, **отделенный** набор элементов когда-то целой параллельной коллекции. И так же, как любой нормальный `Iterator`, `Splitter` становится недействительным после того, как вызван его метод `split`. -Как правило, коллекции разделяются `Splitter`ами на подмножества примерно одинакового размера. В случаях, когда требуются разделы произвольного размера, особенно в параллельных последовательностях, используется `PreciseSplitter`, который является наследником `Splitter` и дополнительно реализует точный метод разделения, `psplit`. +Как правило, коллекции разделяются `Splitter`ами на части примерно одинакового размера. В случаях, когда требуются разделы произвольного размера, особенно в параллельных последовательностях, используется `PreciseSplitter`, который является наследником `Splitter` и дополнительно реализует точный метод разделения, `psplit`. ### Компоновщики Компоновщик `Combiner` можно представить себе как обобщенный `Builder` из библиотеки последовательных коллекций Scala. У каждой параллельной коллекции есть свой отдельный `Combiner`, так же, как у каждой последовательной есть свой `Builder`. -Если в случае с последовательными коллекциями элементы можно добавлять в `Builder`, а потом получить коллекцию, вызвав метод `result`, то при работе с параллельными требуется вызвать у `Combiner`а метод `combine`, который берет аргументом другой `Combiner` и делает новый `Combiner`, который содержит объединенный набор элементов обоих компоновщиков. После вызова метода `combine` оба компоновщика становятся недействительными. +Если в случае с последовательными коллекциями элементы можно добавлять в `Builder`, а потом получить коллекцию, вызвав метод `result`, то при работе с параллельными требуется вызвать у компоновщика метод `combine`, который берет аргументом другой компоновщик и делает новый `Combiner`. Результатом будет компоновщик, содержащий объединенный набор. После вызова метода `combine` оба компоновщика становятся недействительными. trait Combiner[Elem, To] extends Builder[Elem, To] { def combine(other: Combiner[Elem, To]): Combiner[Elem, To] diff --git a/ru/overviews/parallel-collections/configuration.md b/ru/overviews/parallel-collections/configuration.md index 261a8b31a3..4e13ecc36b 100644 --- a/ru/overviews/parallel-collections/configuration.md +++ b/ru/overviews/parallel-collections/configuration.md @@ -13,7 +13,7 @@ num: 7 Параллельные коллекции предоставляют возможность выбора методов планирования задач при выполнении операций. В числе параметров каждой параллельной коллекции есть так называемый объект обслуживания задач, который и отвечает за планирование и распределение нагрузки на процессоры. -Внутри объект обслуживания задач содержит ссылку на реализацию пула потоков; кроме того он определяет, как и когда задачи разбиваются на более мелкие подзадачи. Подробнее о том, как конкретно происходит этот процесс, можно узнать в техническом отчете \[[1][1]\]. +Внутри объект обслуживания задач содержит ссылку на пул потоков; кроме того он определяет, как и когда задачи разбиваются на более мелкие подзадачи. Подробнее о том, как конкретно происходит этот процесс, можно узнать в техническом отчете \[[1][1]\]. В настоящее время для параллельных коллекций доступно несколько реализаций объекта поддержки задач. Например, `ForkJoinTaskSupport` реализован посредством "fork-join" пула и используется по умолчанию на JVM 1.6 или более поздних. Менее эффективный `ThreadPoolTaskSupport` является резервом для JVM 1.5 и тех машин, которые не поддерживают пулы "fork-join". `ExecutionContextTaskSupport` берет реализацию контекста исполнения по умолчанию из `scala.concurrent`, и использует тот же пул потоков, что и `scala.concurrent` (в зависимости от версии JVM, это может быть пул "fork-join" или "thread pool executor"). По умолчанию каждой параллельной коллекции назначается именно обслуживание задач контекста выполнения, поэтому параллельные коллекции используют тот же пул "fork-join", что и API объектов "future". @@ -31,7 +31,7 @@ num: 7 scala> pc map { _ + 1 } res0: scala.collection.parallel.mutable.ParArray[Int] = ParArray(2, 3, 4) -Приведенное выше настраивает параллельную коллекцию на использование "fork-join" пула с уровнем параллелизма равным 2. Заставить коллекцию использовать "thread pool executor" можно так: +Код выше настраивает параллельную коллекцию на использование "fork-join" пула с уровнем параллелизма равным 2. Заставить коллекцию использовать "thread pool executor" можно так: scala> pc.tasksupport = new ThreadPoolTaskSupport() pc.tasksupport: scala.collection.parallel.TaskSupport = scala.collection.parallel.ThreadPoolTaskSupport@1d914a39 @@ -41,7 +41,7 @@ num: 7 Когда параллельная коллекция сериализуется, поле объекта обслуживания задач исключается из сериализуемых. Когда параллельная коллекция восстанавливается из полученной последовательности байт, это поле приобретает значение по умолчанию-- способ обслуживания задач контекста выполнения. -Чтобы реализовать собственный механизм поддержки задач, достаточно расширить трейт `TaskSupport` и реализовать следующие методы: +Чтобы реализовать собственный механизм поддержки задач, достаточно унаследовать трейт `TaskSupport` и реализовать следующие методы: def execute[R, Tp](task: Task[R, Tp]): () => R @@ -49,7 +49,7 @@ num: 7 def parallelismLevel: Int -Метод `execute` планирует асинхронное выполнение задачи и возвращает "future" в качестве ссылки к будущему результату выполнения. Метод `executeAndWait` делает то же самое, но возвращает результат только после завершения задачи. Метод `parallelismLevel` просто возвращает предпочитаемое количество ядер, которые обслуживание задач использует для планирования заданий. +Метод `execute` планирует асинхронное выполнение задачи и возвращает "future" в качестве ссылки к будущему результату выполнения. Метод `executeAndWait` делает то же самое, но возвращает результат только после завершения задачи. Метод `parallelismLevel` просто возвращает предпочитаемое количество ядер, которое будет использовано для вычислений. ## Ссылки diff --git a/ru/overviews/parallel-collections/conversions.md b/ru/overviews/parallel-collections/conversions.md index 61d3754814..dc50771e57 100644 --- a/ru/overviews/parallel-collections/conversions.md +++ b/ru/overviews/parallel-collections/conversions.md @@ -1,4 +1,4 @@ ---- +--- layout: overview-large title: Преобразования параллельных коллекций @@ -26,7 +26,7 @@ num: 3 | `HashMap` | `ParHashMap` | | `HashSet` | `ParHashSet` | -Другие коллекции, такие, как списки, очереди или потоки, последовательны по своей сути, в том смысле, что элементы должны выбираться один за другим. Такие коллекции преобразуются в свои параллельные альтернативы копированием элементов в схожую параллельную коллекцию. Например, рекурсивный список (functional list) преобразуется в стандартную неизменяемую параллельную последовательность, то есть в параллельный вектор. +Другие коллекции, такие, как списки, очереди или потоки, последовательны по своей сути, в том смысле, что элементы должны выбираться один за другим. Такие коллекции преобразуются в свои параллельные альтернативы копированием элементов в схожую параллельную коллекцию. Например, односвязный список преобразуется в стандартную неизменяемую параллельную последовательность, то есть в параллельный вектор. Любая параллельная коллекция может быть преобразована в её последовательный вариант вызовом метода `seq`. Конвертирование параллельной коллекции в последовательную эффективно всегда-- оно занимает постоянное время. Вызов `seq` на изменяемой параллельной коллекции возвращает последовательную, которая отображает ту же область памяти-- изменения, сделанные в одной коллекции, будут видимы в другой. diff --git a/ru/overviews/parallel-collections/ctries.md b/ru/overviews/parallel-collections/ctries.md index e495793200..8af10c87c3 100644 --- a/ru/overviews/parallel-collections/ctries.md +++ b/ru/overviews/parallel-collections/ctries.md @@ -1,6 +1,6 @@ ---- +--- layout: overview-large -title: Многопоточные нагруженные деревья +title: Многопоточные префиксные деревья disqus: true @@ -9,7 +9,7 @@ language: ru num: 4 --- -Большинство многопоточных структур данных не гарантирует правильности последовательного перебора элементов в случае, если эта структура изменяется во время прохождения. То же верно, кстати, и в случае большинства изменяемых коллекций. Особенность многопоточных нагруженных деревьев (также известных, как префиксные деревья) -- `tries`-- заключается в том, что они позволяют модифицировать само дерево, которое в данный момент просматривается. Сделанные изменения становятся видимыми только при следующем прохождении. Так ведут себя и последовательные нагруженные деревья, и их параллельные аналоги; единственное отличие-- в том, что первые перебирают элементы последовательно, а вторые-- параллельно. +Большинство многопоточных структур данных не гарантирует неизменности порядка элементов в случае, если эта структура изменяется во время прохождения. То же верно, кстати, и в случае большинства изменяемых коллекций. Особенность многопоточных префиксных деревьев-- `tries`-- заключается в том, что они позволяют модифицировать само дерево, которое в данный момент просматривается. Сделанные изменения становятся видимыми только при следующем прохождении. Так ведут себя и последовательные префиксные деревья, и их параллельные аналоги; единственное отличие-- в том, что первые перебирают элементы последовательно, а вторые-- параллельно. Это замечательное свойство позволяет упростить ряд алгоритмов. Обычно это такие алгоритмы, в которых некоторый набор данных обрабатывается итеративно, причем для обработки различных элементов требуется различное количество итераций. @@ -98,7 +98,7 @@ num: 4 На GitHub есть пример реализации игры "Жизнь", который использует многопоточные хэш-деревья-- `Ctries`, чтобы выборочно симулировать только те части механизма игры, которые в настоящий момент активны \[[4][4]\]. Он также включает в себя основанную на `Swing` визуализацию, которая позволяет посмотреть, как подстройка параметров влияет на производительность. -Многопоточные нагруженные деревья также поддерживают атомарную, неблокирующую операцию `snapshot`, выполнение которой осуществляется за постоянное время. Эта операция создает новое многопоточное дерево со всеми элементами на некоторый выбранный момент времени, создавая таким образом снимок состояния дерева в этот момент. +Многопоточные префиксные деревья также поддерживают атомарную, неблокирующую(lock-free) операцию `snapshot`, выполнение которой осуществляется за постоянное время. Эта операция создает новое многопоточное дерево со всеми элементами на некоторый выбранный момент времени, создавая таким образом снимок состояния дерева в этот момент. На самом деле, операция `snapshot` просто создает новый корень дерева. Последующие изменения отложенно перестраивают ту часть многопоточного дерева, которая соответствует изменению, и оставляет нетронутой ту часть, которая не изменилась. Прежде всего это означает, что операция 'snapshot' сама по себе не затратна, так как не происходит копирования элементов. Кроме того, так как оптимизация "копирования при записи" создает копии только измененных частей дерева, последующие модификации горизонтально масштабируемы. Метод `readOnlySnapshot` чуть более эффективен, чем метод `snapshot`, но он возвращает неизменяемый ассоциативный массив, который доступен только для чтения. Многопоточные деревья также поддерживают атомарную операцию постоянного времени `clear`, основанную на рассмотренном механизме снимков. Чтобы подробнее узнать о том, как работают многопоточные деревья и их снимки, смотрите \[[1][1]\] и \[[2][2]\]. diff --git a/ru/overviews/parallel-collections/custom-parallel-collections.md b/ru/overviews/parallel-collections/custom-parallel-collections.md index 5d5e44e4f8..b71a0c87b7 100644 --- a/ru/overviews/parallel-collections/custom-parallel-collections.md +++ b/ru/overviews/parallel-collections/custom-parallel-collections.md @@ -1,4 +1,4 @@ ---- +--- layout: overview-large title: Создание пользовательской параллельной коллекции @@ -51,7 +51,7 @@ num: 6 def dup = new ParStringSplitter(s, i, ntl) -И наконец, методы `split` и `psplit`, которые используются для создания разделителей, перебирающих подмножества элементов текущего разделителя. Для метода `split` действует соглашение, что он возвращает последовательность разделителей, перебирающих непересекающиеся подмножества элементов текущего разделителя, ни одно из которых не является пустым. Если текущий разделитель содержит один или менее элементов, `split` возвращает саму последовательность этого разделителя. Метод `psplit` должен возвращать последовательность разделителей, перебирающих точно такое количество элементов, которое задано значениями размеров, указанных параметром `sizes`. Если параметр `sizes` требует отделить меньше элементов, чем содержит текущий разделитель, то дополнительный разделитель со всеми остальными элементами размещается в конце последовательности. Если в параметре `sizes` указано больше элементов, чем содержится в текущем разделителе, для каждого размера, на который не хватило элементов, будет добавлен пустой разделитель. Наконец, вызов `split` или `psplit` делает текущий разделитель недействительным. +И наконец, методы `split` и `psplit`, которые используются для создания разделителей, перебирающих подмножества элементов текущего разделителя. Для метода `split` действует соглашение, что он возвращает последовательность разделителей, перебирающих непересекающиеся подмножества элементов текущего разделителя, ни одно из которых не является пустым. Если текущий разделитель покрывает один или менее элементов, `split` возвращает саму последовательность этого разделителя. Метод `psplit` должен возвращать последовательность разделителей, перебирающих точно такое количество элементов, которое задано значениями размеров, указанных параметром `sizes`. Если параметр `sizes` требует отделить меньше элементов, чем покрыто текущим разделителем, то дополнительный разделитель со всеми остальными элементами размещается в конце последовательности. Если в параметре `sizes` указано больше элементов, чем содержится в текущем разделителе, для каждого размера, на который не хватило элементов, будет добавлен пустой разделитель. Наконец, вызов `split` или `psplit` делает текущий разделитель недействительным. def split = { val rem = remaining @@ -130,11 +130,11 @@ num: 6 } -## Как мне реализовать собственный компоновщик? В общих чертах? +## Как реализовать собственный компоновщик? Тут нет стандартного рецепта, -- все зависит от имеющейся структуры данных, и обычно требует изобретательности со стороны того, кто пишет реализацию. Тем не менее, можно выделить несколько подходов, которые обычно применяются: -1. Конкатенация и объединение. Некоторые структуры данных позволяют реализовать эти операции эффективно (обычно с логарифмической сложностью), и если требуемая коллекция представлена такой структурой данных, ее компоновщик может быть самой такой коллекцией. Особенно хорошо этот подход работает для пальчиковых деревьев (finger trees), веревочных деревьев (ropes) и различных видов куч. +1. Конкатенация и объединение. Некоторые структуры данных позволяют реализовать эти операции эффективно (обычно с логарифмической сложностью), и если требуемая коллекция представлена такой структурой данных, ее компоновщик может быть самой такой коллекцией. Особенно хорошо этот подход работает для подвешенных деревьев (finger trees), веревок (ropes) и различных видов куч. 2. Двухфазное выполнение. Подход, применяемый в случае параллельных массивов и параллельных хэш-таблиц; он предполагает, что элементы могут быть эффективно рассортированы по готовым для конкатенации блокам, из которых результирующая структура данных может быть построена параллельно. В первую фазу блоки заполняются независимо различными процессорами, и в конце просто соединяются конкатенацией. Во вторую фазу происходит выделение памяти для целевой структуры данных, и после этого различные процессоры заполняют различные ее части, используя элементы непересекающихся блоков. Следует принять меры для того, чтобы различные процессоры никогда не изменяли одну и ту же часть структуры данных, иначе не избежать трудноуловимых, связанных с многопоточностью ошибок. Такой подход легко применить к последовательностям с произвольным доступом, как было показано в предыдущем разделе. diff --git a/ru/overviews/parallel-collections/performance.md b/ru/overviews/parallel-collections/performance.md index f6676c516c..77ac294817 100644 --- a/ru/overviews/parallel-collections/performance.md +++ b/ru/overviews/parallel-collections/performance.md @@ -18,7 +18,7 @@ language: ru Кроме этого, результат может включать в себя потраченное на стороне JVM время на осуществление операций автоматического управления памятью. Время от времени выполнение программы прерывается и вызывается сборщик мусора. Если исследуемая программа размещает хоть какие-нибудь данные в куче (а большинство программ JVM размещают), значит сборщик мусора должен запуститься, возможно, исказив при этом результаты измерений. Можно нивелировать влияние сборщика мусора на результат, запустив измеряемую программу множество раз, и тем самым спровоцировав большое количество циклов сборки мусора. -Одной из распространенных причин ухудшения производительности является оборачивание и разворачивание (boxing и unboxing), неявно происходящее в случаях, когда примитивный тип передается аргументом в обобщенный (generic) метод. Чтобы примитивные типы можно было передать в метод с параметром обобщенного типа, они во время выполнения преобразуются в представляющие их объекты. Этот процесс замедляет выполнение, а кроме того порождает необходимость в дополнительном выделении памяти и, соответственно, создает дополнительный мусор в куче. +Одной из распространенных причин ухудшения производительности является упаковка и распаковка примитивов, которые неявно происходят в случаях, когда примитивный тип передается аргументом в обобщенный (generic) метод. Чтобы примитивные типы можно было передать в метод с параметром обобщенного типа, они во время выполнения преобразуются в представляющие их объекты. Этот процесс замедляет выполнение, а кроме того порождает необходимость в дополнительном выделении памяти и, соответственно, создает дополнительный мусор в куче. В качестве распространенной причины ухудшения параллельной производительности можно назвать соперничество за память (memory contention), возникающее из-за того, что программист не может явно указать, где следует размещать объекты. Фактически, из-за влияния сборщика мусора, это соперничество может произойти на более поздней стадии жизни приложения, а именно после того, как объекты начнут перемещаться в памяти. Такие влияния нужно учитывать при написании теста. From 54cc1021aea1c25a8c8c233c2465f0054ddfe862 Mon Sep 17 00:00:00 2001 From: ReturnedToLife Date: Mon, 27 Mar 2017 11:56:32 +0300 Subject: [PATCH 12/12] bug fix 2 --- ru/overviews/parallel-collections/configuration.md | 8 ++++---- ru/overviews/parallel-collections/performance.md | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/ru/overviews/parallel-collections/configuration.md b/ru/overviews/parallel-collections/configuration.md index 4e13ecc36b..fcf72c2f26 100644 --- a/ru/overviews/parallel-collections/configuration.md +++ b/ru/overviews/parallel-collections/configuration.md @@ -11,11 +11,11 @@ num: 7 ## Обслуживание задач -Параллельные коллекции предоставляют возможность выбора методов планирования задач при выполнении операций. В числе параметров каждой параллельной коллекции есть так называемый объект обслуживания задач, который и отвечает за планирование и распределение нагрузки на процессоры. +Параллельные коллекции предоставляют возможность выбора методов планирования задач и распределения нагрузки на процессоры. В числе параметров каждой параллельной коллекции есть так называемый объект обслуживания задач, который и отвечает за это планирование. Внутри объект обслуживания задач содержит ссылку на пул потоков; кроме того он определяет, как и когда задачи разбиваются на более мелкие подзадачи. Подробнее о том, как конкретно происходит этот процесс, можно узнать в техническом отчете \[[1][1]\]. -В настоящее время для параллельных коллекций доступно несколько реализаций объекта поддержки задач. Например, `ForkJoinTaskSupport` реализован посредством "fork-join" пула и используется по умолчанию на JVM 1.6 или более поздних. Менее эффективный `ThreadPoolTaskSupport` является резервом для JVM 1.5 и тех машин, которые не поддерживают пулы "fork-join". `ExecutionContextTaskSupport` берет реализацию контекста исполнения по умолчанию из `scala.concurrent`, и использует тот же пул потоков, что и `scala.concurrent` (в зависимости от версии JVM, это может быть пул "fork-join" или "thread pool executor"). По умолчанию каждой параллельной коллекции назначается именно обслуживание задач контекста выполнения, поэтому параллельные коллекции используют тот же пул "fork-join", что и API объектов "future". +В настоящее время для параллельных коллекций доступно несколько реализаций объекта поддержки задач. Например, `ForkJoinTaskSupport` реализован посредством "fork-join" пула и используется по умолчанию на JVM 1.6 или более поздних. Менее эффективный `ThreadPoolTaskSupport` является резервом для JVM 1.5 и тех машин, которые не поддерживают пулы "fork-join". `ExecutionContextTaskSupport` по умолчанию берет из `scala.concurrent` объект контекста выполнения `ExecutionContext` и, таким образом, использует тот же пул потоков, что и `scala.concurrent` (в зависимости от версии JVM, это может быть пул "fork-join" или "thread pool executor"). По умолчанию каждой параллельной коллекции назначается именно обслуживание задач контекста выполнения, поэтому параллельные коллекции используют тот же пул "fork-join", что и API объектов "future". Сменить метод обслуживания задач для параллельной коллекции можно так: @@ -31,7 +31,7 @@ num: 7 scala> pc map { _ + 1 } res0: scala.collection.parallel.mutable.ParArray[Int] = ParArray(2, 3, 4) -Код выше настраивает параллельную коллекцию на использование "fork-join" пула с уровнем параллелизма равным 2. Заставить коллекцию использовать "thread pool executor" можно так: +Код выше настраивает параллельную коллекцию на использование "fork-join" пула с количеством потоков равным 2. Заставить коллекцию использовать "thread pool executor" можно так: scala> pc.tasksupport = new ThreadPoolTaskSupport() pc.tasksupport: scala.collection.parallel.TaskSupport = scala.collection.parallel.ThreadPoolTaskSupport@1d914a39 @@ -39,7 +39,7 @@ num: 7 scala> pc map { _ + 1 } res1: scala.collection.parallel.mutable.ParArray[Int] = ParArray(2, 3, 4) -Когда параллельная коллекция сериализуется, поле объекта обслуживания задач исключается из сериализуемых. Когда параллельная коллекция восстанавливается из полученной последовательности байт, это поле приобретает значение по умолчанию-- способ обслуживания задач контекста выполнения. +Когда параллельная коллекция сериализуется, поле объекта обслуживания задач исключается из сериализуемых. Когда параллельная коллекция восстанавливается из полученной последовательности байт, это поле приобретает значение по умолчанию, то есть способ обслуживания задач берется из `ExecutionContext`. Чтобы реализовать собственный механизм поддержки задач, достаточно унаследовать трейт `TaskSupport` и реализовать следующие методы: diff --git a/ru/overviews/parallel-collections/performance.md b/ru/overviews/parallel-collections/performance.md index 77ac294817..973e2b049b 100644 --- a/ru/overviews/parallel-collections/performance.md +++ b/ru/overviews/parallel-collections/performance.md @@ -57,10 +57,9 @@ language: ru java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=300000 Map 10 -Флаг `server` требует использовать серверную VM. Флаг `cp` означает "classpath", то есть указывает, что файлы классов требуется искать в текущем каталоге и в jar-архиве библиотеки Scala. Аргументы `-Dpar` и `-Dlength`-- это уровень параллелизма и количество элементов соответственно. Наконец, `10` означает что тест производительности будет запущен на одной и той же JVM именно 10 раз. +Флаг `server` требует использовать серверную VM. Флаг `cp` означает "classpath", то есть указывает, что файлы классов требуется искать в текущем каталоге и в jar-архиве библиотеки Scala. Аргументы `-Dpar` и `-Dlength`-- это количество потоков и количество элементов соответственно. Наконец, `10` означает что тест производительности будет запущен на одной и той же JVM именно 10 раз. -Устанавливая уровень параллелизма `par` в `1`, `2`, `4` и `8`, получаем следующее время выполнения -на четырехъядерном i7 с поддержкой гиперпоточности: +Устанавливая количество потоков `par` в `1`, `2`, `4` и `8`, получаем следующее время выполнения на четырехъядерном i7 с поддержкой гиперпоточности: Map$ 126 57 56 57 54 54 54 53 53 53 Map$ 90 99 28 28 26 26 26 26 26 26