当在F#中生成Primes时,为什么“Eratosthenes的筛子”在这个特定的实现中如此缓慢?

时间:2012-08-17 23:06:02

标签: f#

IE,

我在这里做错了什么?是否必须使用列表,序列和数组以及限制的工作方式?

所以这是设置:我正在尝试生成一些素数。我看到有十亿个素数的十亿个文本文件。问题不是为什么......问题是那些使用python的人如何在this post上计算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秒的CPU 100000 ...之后,它就会爆炸。

4 个答案:

答案 0 :(得分:5)

您的筛选功能很慢,因为您尝试过滤掉最多top_number的复合数字。使用Eratosthenes的Sieve,你只需要这样做,直到sqrt(top_number),剩下的数字本来就是素数。假设我们有top_number = 1,000,000,你的函数进行78498轮次过滤(质数的数量,直到1,000,000),而原始筛子只执行168次(质数的数量)直到1,000)。

除了2之外,你可以避免产生偶数数字,这些数字从一开始就不是素数。此外,sievesieve_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 

在我的机器中,更新版本非常快,并且在top_number = 1,000,000内完成了0.6秒。

答案 1 :(得分:4)

根据我的代码:stackoverflow.com/a/8371684/124259

获取fsi中22毫秒内的前100万个素数 - 很大一部分可能是在此时编译代码。

#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

答案 2 :(得分:2)

对于使用列表(@pad)的一般试验分割算法以及使用Eratosthenes筛选(SoE)筛选筛选数据结构的数组,已经有好几个答案(@John Palmer和@Jon Harrop) 。然而,@ pad的列表算法并不是特别快,并且会为更大的筛选范围“爆炸”而@John Palmer的阵列解决方案稍微复杂一些,使用的内存比必要的多,并且使用外部可变状态所以与如果程序是用C#等命令式语言编写的。

EDIT_ADD:我编辑了下面的代码(带有行注释的旧代码)修改序列表达式以避免一些函数调用,以便反映更多的“迭代器样式”并保存20%的速度仍然没有接近真正的C#迭代器的速度,这与“滚动你自己的枚举器”最终F#代码的速度大致相同。我相应地修改了下面的时间信息。的 END_EDIT

以下真正的SoE程序仅使用64 KB的内存来筛选高达一百万的素数(由于仅考虑奇数并使用打包位BitArray)并且仍然几乎与@John Palmer的程序一样快,大约40毫秒只需几行代码即可在i7 2700K(3.5 GHz)上筛选到100万个:

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减少到大约三次。这与写入的C#代码大致相同。相同的风格。我在第一个例子中使用的F#迭代器样式并没有像C#迭代器那样自动生成IEnumerable样板代码,这太糟糕了,但我想这就是序列的意图 - 只是因为它们如此低效以至于加速性能由于被实现为序列计算表达式。

现在,只有不到一半的时间用于枚举主要结果,以便更好地利用CPU时间。

答案 3 :(得分:1)

  

我在这里做错了什么?

您已经实施了一个不同的算法,该算法遍历每个可能的值,并使用%来确定是否需要删除它。你应该做的是通过固定增量逐步删除倍数。那将是渐近的。

您无法有效地浏览列表,因为它们不支持随机访问,因此请使用数组。