在F#中生成质数时,为什么在此特定实现中“ Eratosthenes筛”这么慢?


When Generating Primes in F#, why is the “Sieve of Erosthenes” so slow in this particular implementatIon?

IE浏览器

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

因此,这里是设置:我正在尝试生成一些素数。 我看到有十亿个素数的十亿个文本文件。 问题不是为什么...问题是这个帖子上的人如何使用python计算毫秒内所有低于1,000,000的素数...而下面的F#代码在做什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
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中启用计时器后,我得到100,000个CPU的价值为4.33秒……在那之后,这一切都爆了。


您的筛选功能很慢,因为您尝试筛选出top_number以下的复合数字。使用Eratosthenes筛网,您只需要这样做,直到sqrt(top_number)和剩余的数本质上都是质数。假设我们有top_number = 1,000,000,则您的函数执行78498次过滤(直到1,000,000的素数),而原始筛只进行168次(直到1,000的素数)。

您可以避免生成偶数,但2不能从一开始就是素数。此外,sievesieve_prime可以合并为递归函数。您可以使用轻量级的List.filter代替List.choose

结合以上建议:

1
2
3
4
5
6
7
8
9
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秒内完成。


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

在22毫秒内获得fsi的前100万个素数-很重要的一点可能是此时正在编译代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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


对于使用列表的常规试验划分算法(@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)上只有一百万行,只有几行代码:

1
2
3
4
5
6
7
8
9
10
11
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个函数调用返回的代价,因此几乎所有经过的时间都花在最后两行中,以枚举找到的质数每次迭代。通过滚动我们自己的迭代器,可以减少每次迭代的几个函数调用,但是代码并不那么简洁。请注意,在下面的代码中,除了筛分数组的内容和使用对象表达式实现迭代器所必需的引用变量之外,没有暴露任何可变状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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毫秒的时间内将同一机器上的质数筛选到100万,这是因为每次迭代的函数调用数量从大约16大大减少到了大约3。这与以相同样式编写的C#代码的速度大致相同。我在第一个示例中使用的F#迭代器样式无法像C#迭代器那样自动生成IEnumerable样板代码,这很糟糕,但是我想这是序列的意图-只是它们是如此低效以至于无法提高性能由于被实现为序列计算表达式。

现在,在列举主要结果上花费了不到一半的时间,以更好地利用CPU时间。


What am I doing wrong here?

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

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