При генерации 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
Что я здесь делаю неправильно?
Вы внедрили другой алгоритм, который проходит через каждое возможное значение, и использует %
, чтобы определить, нужно ли его удалять. То, что вы должны делать, - это шаг за шагом с фиксированным приращением, удаляющим кратные. Это было бы асимптотически.
Вы не можете эффективно выполнять списки, потому что они не поддерживают произвольный доступ, поэтому используйте массивы.