提高序列的性能

Improving the performance of sequence

我正在实现埃拉托色尼筛法的两个版本,第一个是命令式的:

let primesUntilArray n =
    let isqrt x =
        let mutable root = 0UL
        let mutable p = 1UL <<< 31

        while p > 0UL do
            while x < (root + p) * (root + p) do
                p <- p >>> 1

            root <- root + p
            p <- p >>> 1

        root

    let isPrime = Array.create (int <| n + 1) true
    let bound = int <| isqrt (uint64 n)

    let mutable i = 2

    while i <= bound do
        if isPrime.[i] then
            let mutable j = i * i

            while j <= n do
                isPrime.[j] <- false
                j <- j + i

        i <- i + 1

    let mutable primes = []
    let mutable i = 2

    while i <= n do
        if isPrime.[i] then
            primes <- i :: primes

        i <- i + 1

    List.rev primes

而第二个功能正常(使用 F# 的序列):

let primesUntilSequence n =
    let rec pickPrimes s =
        let sieveWith p =
            Seq.filter (fun n -> n < p * p || n % p <> 0)

        let p = Seq.head s

        seq {
            yield p
            yield! pickPrimes (sieveWith p (Seq.tail s))
        }

    Seq.initInfinite (fun i -> i + 2)
    |> pickPrimes
    |> Seq.takeWhile (fun p -> p <= n)
    |> Seq.toList

两者的时间复杂度都在O(n log log n)左右,但是使用sequence的版本性能很差,例如

> primesUntilArray 9000 |> List.length |> printfn "%d";;
1117
Real: 00:00:00.004, CPU: 00:00:00.000, GC Gen0: 1, Gen1: 0, Gen2: 0
val it: unit = ()

> primesUntilSequence 9000 |> List.length |> printfn "%d";;
1117
Real: 00:00:15.388, CPU: 00:00:15.375, GC Gen0: 592, Gen1: 64, Gen2: 0
val it: unit = ()

这意味着直到 9000 时生成素数的速度要慢大约 4000 倍。我怎样才能提高第二个素数的性能?

谢谢大家。 以下是@Tomas Petricek

解决方案的略微修改版本
let primesUntilList n =
    let rec pickPrimes ps xs =
        let sieveWith p =
            List.filter (fun n -> n < p * p || n % p <> 0)

        match xs with
        | p :: xs' ->
            if p * p > n then
                (List.rev xs) @ ps
            else
                pickPrimes (p :: ps) (sieveWith p xs')
        | _ -> ps

    List.rev <| pickPrimes [] [ 2..n ]

这个基于列表的版本仍然比使用数组的效率低两倍,但比我最初的基于序列的版本要好得多。

基于序列的版本的问题是使用 Seq.headSeq.tail 递归地解构序列是非常低效的。 Seq.tail 返回的序列迭代原始序列,但跳过第一个元素。这意味着通过递归地应用 Seq.tail,您将创建越来越多的需要迭代的序列(我猜这是 O(N^2))。

如果你使用一个列表,这会变得更有效率,其中针对 x::xs 的模式匹配只是引用下一个缺点单元格:

let primesUntilList n =
  let rec pickPrimes s =
      let sieveWith p =
          List.filter (fun n -> n < p * p || n % p <> 0)
      match s with 
      | [] -> []
      | p::ps -> p :: pickPrimes (sieveWith p ps)

  [ 2 .. n ] |> pickPrimes

这仍然比基于数组的版本效率低(我认为这是可以预料的),但它的性能不如基于序列的版本。

Seq.cache 当您想多次评估一个序列时很有用(这实际上是 Seq.tail 所做的)。您可以像这样将其放入原始代码中:

let primesUntilSequence n =
    let rec pickPrimes s =
        let sieveWith p =
            Seq.filter (fun n -> n < p * p || n % p <> 0)

        let s = Seq.cache s   // *** cache the sequence ***
        let p = Seq.head s

        seq {
            yield p
            yield! pickPrimes (sieveWith p (Seq.tail s))
        }

    Seq.initInfinite (fun i -> i + 2)
    |> pickPrimes
    |> Seq.takeWhile (fun p -> p <= n)
    |> Seq.toList

在我的盒子上,primesUntilSequence 9000 现在运行大约四分之一秒。

在处理序列性能问题时,在序列表达式中命令式地处理它们会很有帮助。简而言之,带上你自己的枚举器。

arraylist 方法相比,这仍然非常低效。至少您可以完全控制嵌套序列所需的资源分配。

type IEnumerator<'a> =
    System.Collections.Generic.IEnumerator<'a>
let primesUntilIterator n =
    let sieveWith p (en : IEnumerator<_>) = seq{ 
        while en.MoveNext() do
            let n = en.Current
            if n < p * p || n % p <> 0 then
                yield n }
    let rec pickPrimes (s : seq<_>) = seq{
        use en = s.GetEnumerator()
        if en.MoveNext() then
            let p = en.Current
            yield p
            yield! pickPrimes (sieveWith p en) }

    { 2..n } |> pickPrimes