При генерации Primes в F #, почему "Сито из Erosthenes" настолько медленное в этом конкретном исполнении?

IE,

Что я здесь делаю неправильно? Нужно ли это со списками, последовательностями и массивами и тем, как работают ограничения?

Итак, вот настройка: я пытаюсь сгенерировать некоторые простые числа. Я вижу, что есть миллиард текстовых файлов из миллиарда простых чисел. Вопрос не в том, почему... вопрос в том, как ребята используют python, вычисляя все простые числа ниже 1,000,000 в миллисекундах на этом сообщении... и что Я делаю неправильно со следующим кодом F #?

let sieve_primes2 top_number = 
    let numbers = [ for i in 2 .. top_number do yield i ]
    let sieve (n:int list) = 
        match n with
        | [x] -> x,[]
        | hd :: tl -> hd, List.choose(fun x -> if x%hd = 0 then None else Some(x)) tl
        | _ -> failwith "Pernicious list error."
    let rec sieve_prime (p:int list) (n:int list) =  
        match (sieve n) with
        | i,[] -> i::p
        | i,n'  -> sieve_prime (i::p) n'
    sieve_prime [1;0] numbers 

Когда таймер включен в FSI, я получаю процессор на 4,33 секунды для 100000... после этого все просто взрывается.

Ответ 1

Ваша функция сита медленная, потому что вы пытались отфильтровать составные числа до top_number. С Sieve of Eratosthenes вам нужно сделать это только до sqrt(top_number), а оставшиеся номера по сути являются первичными. Предположим, что мы имеем top_number = 1,000,000, ваша функция делает 78498 раунды фильтрации (количество простых чисел до 1,000,000), в то время как исходное сито делает это только 168 раз (количество простых чисел до 1,000).

Вы можете избежать генерации четных чисел, кроме 2, которые не могут быть начальными с начала. Более того, sieve и sieve_prime могут быть объединены в рекурсивную функцию. И вы можете использовать легкий List.filter вместо List.choose.

Включая выше предложения:

let sieve_primes top_number = 
    let numbers = [ yield 2
                    for i in 3..2..top_number -> i ]
    let rec sieve ns = 
        match ns with
        | [] -> []
        | x::xs when x*x > top_number -> ns
        | x::xs -> x::sieve (List.filter(fun y -> y%x <> 0) xs)
    sieve numbers 

В моей машине обновленная версия выполняется очень быстро, и она заканчивается в пределах 0,6 с для top_number = 1,000,000.

Ответ 2

На основе моего кода здесь: stackoverflow.com/a/8371684/124259

Получает первые 1 миллион простых чисел в 22 миллисекундах в fsi - значительная часть, вероятно, компилирует код в этот момент.

#time "on"

let limit = 1000000
//returns an array of all the primes up to limit
let table =
    let table = Array.create limit true //use bools in the table to save on memory
    let tlimit = int (sqrt (float limit)) //max test no for table, ints should be fine
    let mutable curfactor = 1;
    while curfactor < tlimit-2 do
        curfactor <- curfactor+2
        if table.[curfactor]  then //simple optimisation
            let mutable v = curfactor*2
            while v < limit do
                table.[v] <- false
                v <- v + curfactor
    let out = Array.create (100000) 0 //this needs to be greater than pi(limit)
    let mutable idx = 1
    out.[0]<-2
    let mutable curx=1
    while curx < limit-2 do
        curx <- curx + 2
        if table.[curx] then
            out.[idx]<-curx
            idx <- idx+1
    out

Ответ 3

Было несколько хороших ответов как на общий алгоритм деления с использованием списков (@pad), так и на выбор массива для структуры данных просеивания с использованием сита Эратосфена (SoE) (@John Palmer и @Jon Harrop), Однако алгоритм списка @pad не особенно быстрый и будет "взрываться" для больших диапазонов просеивания, а решение массива @John Palmer несколько сложнее, использует больше памяти, чем необходимо, и использует внешнее изменяемое состояние, поэтому не отличается от того, программа была написана на императивном языке, таком как С#.

EDIT_ADD: Я редактировал приведенный ниже код (старый код с комментариями строк), изменяя выражение последовательности, чтобы избежать некоторых вызовов функций, чтобы отразить больше "стиля итератора", и при сохранении 20% скорости все еще не приближается к скорости истинного итератора С#, который примерно такой же, как и конечный F # -код "сворачивать ваш собственный перечислитель". Я соответствующим образом изменил информацию о времени. END_EDIT

Следующая истинная программа SoE использует только 64 Кбайт памяти для просеивания простых чисел до миллиона (из-за учета только нечетных чисел и использования упакованного бита BitArray) и все еще почти так же быстро, как программа @John Palmer примерно через 40 миллисекунд чтобы просеять до миллиона на i7 2700K (3,5 ГГц), всего лишь с несколькими строками кода:

open System.Collections
let primesSoE top_number=
  let BFLMT = int((top_number-3u)/2u) in let buf = BitArray(BFLMT+1,true)
  let SQRTLMT = (int(sqrt (double top_number))-3)/2
  let rec cullp i p = if i <= BFLMT then (buf.[i] <- false; cullp (i+p) p)
  for i = 0 to SQRTLMT do if buf.[i] then let p = i+i+3 in cullp (p*(i+1)+i) p
  seq { for i = -1 to BFLMT do if i<0 then yield 2u
                               elif buf.[i] then yield uint32(3+i+i) }
//  seq { yield 2u; yield! seq { 0..BFLMT } |> Seq.filter (fun i->buf.[i])
//                                          |> Seq.map (fun i->uint32 (i+i+3)) }
primesSOE 1000000u |> Seq.length;;

Почти все прошедшее время проводится в последних двух строках, перечисляющих найденные простые числа из-за неэффективности библиотеки времени выполнения последовательности, а также затрат на перечисление себя примерно на 28 тактов на вызов функции и возврат с 16 вызовов функций на итерацию. Это может быть сведено лишь к нескольким вызовам функций на итерацию, скопировав наш собственный итератор, но код не так краток; обратите внимание, что в следующем коде отсутствует изменяемое состояние, отличное от содержимого массива просеивания, и ссылочная переменная, необходимая для реализации итератора, с помощью объектных выражений:

open System
open System.Collections
open System.Collections.Generic
let primesSoE top_number=
  let BFLMT = int((top_number-3u)/2u) in let buf = BitArray(BFLMT+1,true)
  let SQRTLMT = (int(sqrt (double top_number))-3)/2
  let rec cullp i p = if i <= BFLMT then (buf.[i] <- false; cullp (i+p) p)
  for i = 0 to SQRTLMT do if buf.[i] then let p = i+i+3 in cullp (p*(i+1)+i) p
  let nmrtr() =
    let i = ref -2
    let rec nxti() = i:=!i+1;if !i<=BFLMT && not buf.[!i] then nxti() else !i<=BFLMT
    let inline curr() = if !i<0 then (if !i= -1 then 2u else failwith "Enumeration not started!!!")
                        else let v = uint32 !i in v+v+3u
    { new IEnumerator<_> with
        member this.Current = curr()
      interface IEnumerator with
        member this.Current = box (curr())
        member this.MoveNext() = if !i< -1 then i:=!i+1;true else nxti()
        member this.Reset() = failwith "IEnumerator.Reset() not implemented!!!"
      interface IDisposable with
        member this.Dispose() = () }
  { new IEnumerable<_> with
      member this.GetEnumerator() = nmrtr()
    interface IEnumerable with
      member this.GetEnumerator() = nmrtr() :> IEnumerator }
primesSOE 1000000u |> Seq.length;;

Приведенный выше код занимает около 8,5 миллисекунд, чтобы просеять простые числа до миллиона на одном компьютере из-за значительного сокращения числа вызовов функций на итерацию примерно до трех примерно с 16. Это примерно такая же скорость, что и код С#, написанный на в том же стиле. Слишком плохо, что стиль итератора F #, как я использовал в первом примере, автоматически не генерирует IEnumerable код плиты котла, как итераторы С#, но я предполагаю, что это намерение последовательностей - просто, что они настолько прокляты, что неэффективны в скорости производительности из-за того, что они выполняются как выражения вычисления последовательности.

Теперь менее половины времени затрачивается на перечисление простых результатов для гораздо лучшего использования времени процессора.

Ответ 4

Что я здесь делаю неправильно?

Вы внедрили другой алгоритм, который проходит через каждое возможное значение, и использует %, чтобы определить, нужно ли его удалять. То, что вы должны делать, - это шаг за шагом с фиксированным приращением, удаляющим кратные. Это было бы асимптотически.

Вы не можете эффективно выполнять списки, потому что они не поддерживают произвольный доступ, поэтому используйте массивы.