Почему в Scala мой алгоритм Sieve работает так медленно? - PullRequest
8 голосов
/ 27 октября 2011

Я пытаюсь реализовать Сито Эратосфена, используя списки и фильтры, а не массивы и циклы.Я не уверен, почему следующее работает значительно хуже, чем императивный эквивалент.1 миллион должен летать, но моя машина останавливается.

  val max = 1000000
  def filterPrimes(upper: Int, seed: Int = 2, sieve: List[Int] = List()): List[Int] =
    sieve.map(x => if (x % seed == 0 && x > seed) 0 else x).filter(_ > 0)

  var filtered: List[Int] = (2 to max).toList
  for (i <- 2 to max / 2) filtered = filterPrimes(max, i, filtered)
  filtered.foreach(println(_))

Ответы [ 5 ]

10 голосов
/ 27 октября 2011

Если вы хотите увидеть функциональный способ изготовления сита, посмотрите Подлинное сито эратосфена .

10 голосов
/ 27 октября 2011

Есть несколько потенциальных проблем, хотя на самом деле я не вижу ни одного "дымящего пистолета" ... В любом случае, вот что у меня есть.Во-первых:

sieve.map(x => if (x % seed == 0 && x > seed) 0 else x).filter(_ > 0)

можно записать более кратко, как:

sieve.filter(x => x <= seed || x % seed != 0)

Далее, upper не используется в filterPrimes (хотя это не должно влиять на производительность).

В-третьих, не используйте цикл var и for, если вы действительно хотите использовать чисто функциональный стиль, вместо этого включите filterPrimes в хвостовую рекурсивную функцию.Компилятор может быть достаточно умным, чтобы оптимизировать копирование, если вы сделаете это таким образом (хотя я бы не стал задерживать дыхание).

Наконец, и, возможно, самое главное, ваш цикл for тратит огромные ресурсыколичество времени, отфильтровывающего значения, которые обязательно уже отфильтрованы.Например, он пытается отфильтровать кратные 4 после того, как уже отфильтровали все кратные 2. Если вы хотите эффективно использовать этот алгоритм сита, вам нужно выбрать свои семена из оставшихся элементов в списке.

ВДругими словами, сохраните в списке index и определите начальное число из индекса, например:

iteration 0: 2 3 4 5 6 7 8 9 ...
      index: ^

iteration 1: 2 3 5 7 9 ...
      index:   ^

iteration 2: 2 3 5 7 ...
      index:     ^

, чтобы избежать дублирования.Кроме того, вам не нужно продолжать итерации, пока вы не доберетесь до max, я думаю, что вы действительно можете остановиться, когда доберетесь до sqrt(max).

5 голосов
/ 27 октября 2011

Я бы сделал несколько модификаций.

  • Кажется странным, что вы выполняете filterPrimes для всех чисел между 2 и max / 2, «фактическим»Техника сита требует, чтобы вы выполняли только filterPrimes для всех простых чисел между 2 и sqrt(max).
  • Также кажется странным, что вы используете var и a for loop.Чтобы сделать это «функциональным» способом, я бы вместо этого использовал рекурсивную функцию.
  • Вместо выполнения filterPrimes по всему списку, вы можете собирать простые числа по ходу дела;нет необходимости перебрасывать их через фильтр снова и снова.
  • Довольно странно map, а затем filter, как вы делаете, так как карта просто помечает, какие элементы фильтровать, вы можете выполнитьто же самое, используя только фильтр.

Итак, вот моя первая попытка этих модификаций:

def filterFactors(seed: Int, xs: List[Int]) = {
  xs.filter(x => x % seed != 0)
}

def sieve(max: Int) = {
  def go(xs: List[Int]) : List[Int] = xs match {
    case y :: ys => {
      if (y*y > max) y :: ys
      else y :: go(filterFactors(y, ys))
    }
    case Nil => Nil
  }

  go((2 to max).toList)
}

Однако, это отражает мой уклон Хаскелла и имеет огромный недостаток: он потребуетиз-за рекурсивного вызова y :: go(...) в вспомогательной функции go.Запуск sieve(1000000) привел к «OutOfMemoryError» для меня.

Давайте попробуем обычный трюк FP: хвостовая рекурсия с аккумуляторами.

def sieve(max: Int) = {
  def go(xs: List[Int],
         acc: List[Int])
         : List[Int] = xs match {
    case y :: ys => {
      if (y*y > max) acc.reverse ::: (y :: ys)
      else go(filterFactors(y, ys), y :: acc)
    }
    case Nil => Nil
  }

  go((2 to max).toList, Nil)
}

Добавив значение аккумулятора, мы можем записатьвспомогательная функция go в хвостово-рекурсивной форме, что позволяет избежать проблем с большими стеками.(Стратегия оценки Haskell сильно отличается; поэтому она не нуждается и не извлекает выгоду из хвостовой рекурсии)

Теперь давайте сравним скорость с императивным подходом, основанным на мутациях.

def mutationSieve (max: Int) = {
    var arr: Array[Option[Int]] =
        (2 to max).map (x => Some (x)).toArray
    var i = 0
    var seed = (arr (i)).get
    while (seed * seed < max) {
        for (j: Int <- (i + seed) to (max - 2) by seed) {
          arr (j) = None
        }
        i += 1
        while (arr (i).isEmpty) {
          i += 1
        }
        seed = (arr (i)).get
    }
    arr.flatten
}

Здесь я используюArray[Option[Int]] и «вычеркнуть» число, заменив его запись на «Нет».Есть немного места для оптимизации;возможно, небольшое увеличение скорости можно получить с помощью массива bools, где индекс представляет конкретное число.Как бы то ни было.

Используя очень примитивные технические приемы (аккуратно расставленные new Date() звонки ...), я сравнил функциональную версию примерно в 6 раз медленнее, чем императивная.Ясно, что оба подхода имеют одинаковую сложность времени, но постоянные факторы, связанные с программированием со связанными списками, несут затраты.

Я также сравнил вашу версию, используя Math.sqrt(max).ceil.toInt вместо max / 2: это было примерно в 15 раз медленнее, чем функциональная версия, которую я представил здесь.Интересно, что 1 предполагает, что примерно 1 из каждых 7 чисел от 1 до 1000 (sqrt(1000000)) является простым (1 / ln (1000)), следовательно, Большая часть замедления может быть объяснена тем фактом, что вы выполняете цикл для каждого отдельного числа, в то время как я выполняю свою функцию только для каждого простого числа. Конечно, если для выполнения ~ 1000 итераций потребовалось 15 раз больше времени, для выполнения 500000 итераций потребуется ~ 7500x , поэтому ваш оригинальный код мучительно медленен.

2 голосов
/ 27 октября 2011

Это быстрое сито, реализующее подсказки mergeconflict и некоторые подсказки из статьи, упомянутые Кеном Уэйном Вандером.горизонтальный от 100 000 до 1 000 000 простых чисел.Алгоритм deltaNovember уже улучшен для работы только с math.sqrt (max) и фильтрацией, предложенной Алексеем Романовым в комментарии.У Дэна Бертона я взял второй и последний алгоритм с небольшой модификацией, чтобы он соответствовал моему интерфейсу (список, а не массив) и битовому сите, с которым он только ссылался в комментарии, но который был самым быстрым.

0 голосов
/ 27 октября 2011

Списки неизменны, и каждый вызов filterPrimes создает новый список. Вы создаете много списков, что, кстати, не нужно.

Придерживайтесь вашего первого инстинкта (который вы, вероятно, называете «императивным эквивалентом»), который, как я предполагаю, использует одиночный изменяемый массив.

(Отредактировано, чтобы прояснить, что я поняла, что создание нескольких списков не нужно.)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...