这个素数发生器的执行时间能否得到改善?

时间:2010-01-13 01:05:21

标签: optimization f#

写这篇文章的最初目标是尽可能减少占地面积。我可以充满信心地说,这个目标已经实现。不幸的是,这让我的执行速度相当慢。为了产生低于200万的所有质数,在3Ghz英特尔芯片上需要大约8秒钟。

有没有改善这段代码的执行时间,而对内存占用空间的贡献最小?或者,从功能的角度来看,我是否会以错误的方式解决这个问题?

代码

/// 6.5s for max = 2,000,000
let generatePrimeNumbers max =    
    let rec generate number numberSequence =
        if number * number > max then numberSequence else
        let filteredNumbers = numberSequence |> Seq.filter (fun v -> v = number || v % number <> 0L)
        let newNumberSequence = seq { for i in filteredNumbers -> i }
        let newNumber = newNumberSequence |> Seq.find (fun x -> x > number)
        generate newNumber newNumberSequence                
    generate 2L (seq { for i in 2L..max -> i })

更新

我调整了算法,并设法削减了2秒,但内存消耗增加了一倍。

/// 5.2s for max = 2,000,000
let generatePrimeNumbers max =    
    let rec generate number numberSequence =
        if number * number > max then numberSequence else
        let filteredNumbers = numberSequence |> Seq.filter (fun v -> v = number || v % number <> 0L) |> Seq.toArray |> Array.toSeq
        let newNumber = filteredNumbers |> Seq.find (fun v -> v > number)
        generate newNumber filteredNumbers                
    generate 2L (seq { for i in 2L..max -> i })

更新

显然,我使用的是旧编译器。使用最新版本,我的原始算法需要6.5s而不是8s。这是一个很大的改进。

5 个答案:

答案 0 :(得分:8)

Tomas Petricek's function非常快,但我们可以加快一点。

比较以下内容:

let is_prime_tomas n =
    let ms = int64(sqrt(float(n)))
    let rec isPrimeUtil(m) =
        if m > ms then true
        elif n % m = 0L then false
        else isPrimeUtil(m + 1L)
    (n > 1L) && isPrimeUtil(2L)

let is_prime_juliet n =
    let maxFactor = int64(sqrt(float n))
    let rec loop testPrime tog =
        if testPrime > maxFactor then true
        elif n % testPrime = 0L then false
        else loop (testPrime + tog) (6L - tog)
    if n = 2L || n = 3L || n = 5L then true
    elif n <= 1L || n % 2L = 0L || n % 3L = 0L || n % 5L = 0L then false
    else loop 7L 4L

is_prime_juliet内循环略快一点。它是一个众所周知的素数生成策略,它使用“切换”以2或4的增量跳过非素数。用于比较:

> seq { 2L .. 2000000L } |> Seq.filter is_prime_tomas |> Seq.fold (fun acc _ -> acc + 1) 0;;
Real: 00:00:03.628, CPU: 00:00:03.588, GC gen0: 0, gen1: 0, gen2: 0
val it : int = 148933

> seq { 2L .. 2000000L } |> Seq.filter is_prime_juliet |> Seq.fold (fun acc _ -> acc + 1) 0;;
Real: 00:00:01.530, CPU: 00:00:01.513, GC gen0: 0, gen1: 0, gen2: 0
val it : int = 148933

我的版本速度提高了2.37倍,而且它也非常接近最快命令版本的速度。我们可以使更快因为我们不需要过滤2L .. 2000000L列表,我们可以使用相同的策略在我们应用过滤器之前生成更优化的可能素数序列:

> let getPrimes upTo =
    seq {
        yield 2L;
        yield 3L;
        yield 5L;
        yield! (7L, 4L) |> Seq.unfold (fun (p, tog) -> if p <= upTo then Some(p, (p + tog, 6L - tog)) else None)
    }
    |> Seq.filter is_prime_juliet;;
Real: 00:00:00.000, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0

val getPrimes : int64 -> seq<int64>

> getPrimes 2000000L |> Seq.fold (fun acc _ -> acc + 1) 0;;
Real: 00:00:01.364, CPU: 00:00:01.357, GC gen0: 36, gen1: 1, gen2: 0
val it : int = 148933

我们从1.530s降至01.364s,因此我们的速度提高了1.21倍。真棒!

答案 1 :(得分:7)

只是为了测试,我们来看看this page

pi(x)是素数计数函数,它返回x以下的素数。您可以使用以下不等式近似pi(x):

(x/log x)(1 + 0.992/log x) < pi(x) < (x/log x)(1 + 1.2762/log x) 
// The upper bound holds for all x > 1

p(x)是第n个素数函数,可以使用:

近似
n ln n + n ln ln n - n < p(n) < n ln n + n ln ln n
// for n >= 6

考虑到这一点,here is a very fast generator计算前n个素数,其中索引i的每个元素等于p(i)。因此,如果我们想要将数组限制在低于2,000,000的素数,我们使用上限不等式作为素数计数函数:

let rec is_prime (primes : int[]) i testPrime maxFactor =
    if primes.[i] > maxFactor then true
    else
        if testPrime % primes.[i] = 0 then false
        else is_prime primes (i + 1) testPrime maxFactor

let rec prime_n (primes : int[]) primeCount testPrime tog =
    if primeCount < primes.Length then
        let primeCount' =
            if is_prime primes 2 testPrime (float testPrime |> sqrt |> int) then
                primes.[primeCount] <- testPrime
                primeCount + 1
            else
                primeCount
        prime_n primes primeCount' (testPrime + tog) (6 - tog)

let getPrimes upTo =
    let x = float upTo
    let arraySize = int <| (x / log x) * (1.0 + 1.2762 / log x)
    let primes = Array.zeroCreate (max arraySize 3)
    primes.[0] <- 2
    primes.[1] <- 3
    primes.[2] <- 5

    prime_n primes 3 7 4
    primes

酷!那有多快?在我的3.2ghz四核上,我在fsi中得到以下内容:

> let primes = getPrimes 2000000;;
Real: 00:00:00.534, CPU: 00:00:00.546, GC gen0: 0, gen1: 0, gen2: 0

val primes : int [] =
  [|2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37; 41; 43; 47; 53; 59; 61; 67; 71;
    73; 79; 83; 89; 97; 101; 103; 107; 109; 113; 127; 131; 137; 139; 149; 151;
    157; 163; 167; 173; 179; 181; 191; 193; 197; 199; 211; 223; 227; 229; 233;
    239; 241; 251; 257; 263; 269; 271; 277; 281; 283; 293; 307; 311; 313; 317;
    331; 337; 347; 349; 353; 359; 367; 373; 379; 383; 389; 397; 401; 409; 419;
    421; 431; 433; 439; 443; 449; 457; 461; 463; 467; 479; 487; 491; 499; 503;
    509; 521; 523; 541; ...|]

> printfn "total primes: %i. Last prime: %i" (primes.Length - 1) primes.[primes.Length - 1];;
total primes: 149973. Last prime: 2014853

所以在不到半秒的时间内所有素数都在200万左右:)

答案 2 :(得分:4)

编辑:下面的更新版本,使用更少的内存并且更快

有时能够改变东西是件好事。这是一个肯定相当强制性的版本,它以速度换取记忆。由于这个帖子在F#中主持了一个很好的素数生成函数集合,我认为最好添加我的。使用BitArray可以控制内存占用。

open System.Collections

let getPrimes nmax =
    let sieve = new BitArray(nmax+1, true)
    let result = new ResizeArray<int>(nmax/10)
    let upper = int (sqrt (float nmax))   
    result.Add(2)

    let mutable n = 3
    while n <= nmax do
       if sieve.[n] then
           if n<=upper then 
               let mutable i = n
               while i <= nmax do sieve.[i] <- false; i <- i + n
           result.Add n
       n <- n + 2
    result

更新

上面的代码可以进一步优化:因为它只使用筛子中的奇数索引,BitArray可以通过将奇数n索引为2m + 1来减少到一半大小。新版本也更快:

let getPrimes2 nmax =
    let sieve = new BitArray((nmax/2)+1, true)
    let result = new ResizeArray<int>(nmax/10)
    let upper = int (sqrt (float nmax))   
    if nmax>1 then result.Add(2) //fixes a subtle bug for nmax < 2

    let mutable m = 1
    while 2*m+1 <= nmax do
       if sieve.[m] then
           let n = 2*m+1
           if n <= upper then 
               let mutable i = m
               while 2*i < nmax do sieve.[i] <- false; i <- i + n
           result.Add n
       m <- m + 1
    result

计时(英特尔核心组合2.33GHz):

> getPrimes 2000000 |> Seq.length;;
Real: 00:00:00.037, CPU: 00:00:00.046, GC gen0: 0, gen1: 0, gen2: 0
val it : int = 148933
> getPrimes2 2000000 |> Seq.length;;
Real: 00:00:00.026, CPU: 00:00:00.031, GC gen0: 0, gen1: 0, gen2: 0
val it : int = 148933

答案 3 :(得分:3)

Yin发布的命令式版本非常快。在我的机器上它也是大约0.5秒。 但是,如果您想编写一个简单的功能解决方案,您可以写下:

let isPrime(n) =
  let ms = int64(sqrt(float(n)))
  let rec isPrimeUtil(m) =
    if m > ms then true
    elif n % m = 0L then false
    else isPrimeUtil(m + 1L)
  (n > 1L) && isPrimeUtil(2L)

[ 1L .. 2000000L ] |> List.filter isPrime

这只是测试一个数字是否是所有数字到1百万的素数。它没有使用任何复杂的算法(最简单的解决方案通常足够好,实际上很有趣!)。在我的机器上,您的更新版本运行大约11秒,运行大约2秒。

更有趣的是,这很容易并行化。如果您使用PLINQ,您可以编写下面的版本,它在双核上的运行速度将快2倍。这意味着在四核上,它可以像这里所有答案中最快的解决方案一样快,但只需最少的编程工作:-)(当然,使用四个核不是生态的,但是......好)

[ 1L .. 2000000L ] |> PSeq.ofSeq |> PSeq.filter isPrime |> List.ofSeq

PSeq函数是我为我的书创建的PLINQ的包装器(它使得使用F#中的PLINQ更自然)。它们位于source code for Chapter 14

答案 4 :(得分:2)

我写了一个命令式版本,速度更快。为了达到相同的速度,可能无法以纯函数方式编写Sieve of Eratosthenes,因为每个数字必须具有二进制状态。

let generatePrimes max=
    let p = Array.create (max+1) true
    let rec filter i step = 
        if i <= max then 
            p.[i] <- false
            filter (i+step) step
    {2..int (sqrt (float max))} |> Seq.map (fun i->filter (i+i) i) |> Seq.length |> ignore
    {2..max} |> Seq.filter (fun i->p.[i]) |> Seq.toArray