Scala parallel для нехватки оперативной памяти

Итак, в качестве домашнего задания я должен поиграть с несколькими механизмами многопоточности, используя простую интеграцию функции, которая должна привести к числу пи. Реализация должна обрабатывать интервал более 500 миллиардов. Моя текущая реализация обрабатывает цикл for примерно до 50 миллионов при размере кучи 2 ГБ. Теперь мой вопрос: почему реализация использует так много памяти? (Я думаю, это потому, что диапазон должен быть сделан заранее, это правда?) И как мне улучшить использование памяти? Можно ли сделать с параллельными коллекциями или я вынужден использовать пул потоков для чего-то подобного?

Примечание. Я получу полный кредит со следующей реализацией. Это для моего интеллектуального любопытства и моей мечты стать более беглым в scala.

import scala.Math

object Pi {
 def calculate_pi(interval: Int): Double = {
    val start: Long = System.currentTimeMillis;
    var duration: Long = 0
    var x: Double = 2.0/interval
    var y: Double = 0.0
    var mypi: Double = 0.0

    (x until 1.0 by 1.0/interval).par.foreach(i => {
       y = 4.0/(1.0+x*x)
       mypi += (1.0/interval)*y
     })

   duration = (System.currentTimeMillis - start)
   println("scala calculate_pi\t" + interval + "\t" + duration + "ms\t" + mypi)
   return mypi
 }




object Main extends App {
  println("Please input the interval for the calculation")
  if(args.length < 1) { sys.exit }
  val interval = args(0).toInt 
  Pi.calculate_pi_seq(interval)
  Pi.calculate_pi(interval)
}

person Bbatha    schedule 09.02.2012    source источник
comment
Это вообще работает? Похоже, вы одновременно модифицируете mypi. И, что более приземленно, но имеет большее значение, вы не используете свою переменную цикла i (вместо этого вы используете свою константу x)   -  person Didier Dupont    schedule 09.02.2012
comment
Можете ли вы опубликовать полученную ошибку OutOfMemoryError?   -  person axel22    schedule 09.02.2012
comment
java.lang.OutOfMemoryError: пространство кучи Java   -  person Bbatha    schedule 10.02.2012


Ответы (2)


Это все неправильно:

(x until 1.0 by 1.0/interval).par.foreach(i => {
   y = 4.0/(1.0+x*x)
   mypi += (1.0/interval)*y
 })

Первая проблема заключается в том, что все вычисления y идентичны: вы не используете i при его вычислении. Поскольку x не меняется, все потоки вычисляют одно и то же значение.

И вот вторая проблема: вы вычисляете mypiy) параллельно. Это означает, что несколько потоков читают и записывают как mypi, так и y одновременно.

Давайте рассмотрим одно исполнение, чтобы понять, в чем проблема. Допустим, первый поток начинает работать, вычисляет y, а затем считывает y и mypi. Затем этот поток приостанавливается, и все остальные потоки запускаются. Наконец, этот поток возобновляет работу и записывает результат своих вычислений в mypi. В этом случае все вычисления всех остальных потоков тратятся впустую, потому что окончательное значение было дано этим одним потоком.

Это был простой случай. По сути, вы вообще не можете предсказать, что произойдет для каждого из этих операций чтения и записи в mypi (y проще, так как все потоки присваивают ему одно и то же значение).

И да, когда вы вызываете .par для NumericRange, создается коллекция со всеми значениями этого NumericRange.

person Daniel C. Sobral    schedule 09.02.2012
comment
Спасибо за х * х, не обращал внимания, когда переводил последовательный C. Я не уверен, почему параллельное изменение mypi должно иметь значение, если дополнения к целым числам являются атомарными, если я не ошибаюсь. Очевидно, что в целом это не очень хорошая практика, но я думаю, что в простом случае это должно быть нормально. Есть ли предложение решить эту проблему, т.е. я должен хранить все промежуточные результаты в списке и суммировать его? - person Bbatha; 10.02.2012
comment
Чтение int атомарно, запись int атомарна. Чтение-изменение-запись не является атомарным. (См. потерянное обновление.) Вторая проблема заключается в том, что поток mypi может кэшироваться локально. (См. ключевое слово java volatile.) Один из вариантов — использовать алгоритм уменьшения карты. - person user482745; 29.02.2012

Не зная основного приложения, я из экспериментов узнал, что если вы используете метод par для Range (например), он создается заранее, как вы указали.

Однако похоже, что вы используете коллекции только для того, чтобы воспользоваться преимуществами распараллеливания. Другими словами, для вычисления фрагмента кода, который несколько не связан с самой коллекцией - значение i даже не используется. Таким образом, цикл foreach в значительной степени избыточен, поскольку вас интересуют только значения y и x. Может показаться, что для чего-то, что может выполнить простой цикл for, требуется слишком много работы.

Тем не менее, другие типы параллелизма в Scala довольно просты. Как насчет использования актеров? Они легкие и очень простые. В противном случае рабочие потоки или, возможно, даже потоки Java могут помочь.

person Jens Egholm    schedule 09.02.2012
comment
Э, что? Range не является коллекцией и занимает не больше памяти, чем 1 to 2, чем 1 to 1000000. Я даже не понимаю, что вы имеете в виду, когда используете только коллекции, чтобы воспользоваться преимуществами распараллеливания: похоже, что циклы для меня довольно фундаментальны для этого алгоритма! - person oxbow_lakes; 09.02.2012
comment
Конечно есть :) Это просто набор значений от одного к другому. Прошу прощения за мой неудачный выбор слов, но я имел в виду, что если вы используете метод par, это приведет к запуску scala и вычислению каждого элемента в коллекции (чего нет в Range или Stream) . Что касается использования коллекций для использования преимуществ распараллеливания, я думаю, это довольно очевидно, когда используется метод par(). Par — это просто наборы scala, представляющие разумную альтернативу созданию потоков. Отсюда преимущество сборников. - person Jens Egholm; 09.02.2012