【问题标题】:Improving the performance of sequence提高序列的性能
【发布时间】:2022-01-17 16:15:45
【问题描述】:

我正在实现两个版本的 Eratosthenes's Sieve,第一个是必要的:

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 = ()

这意味着生成素数要慢大约 4000 倍,直到 9000。如何提高第二个的性能?

谢谢大家。 以下是@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 ]

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

【问题讨论】:

    标签: .net f# primes


    【解决方案1】:

    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 现在运行大约四分之一秒。

    【讨论】:

    • 非常感谢。您的分析很准确,我认为Seq.tailO(1),但事实并非如此。我不知道Seq.cache;如果我理解正确let s = Seq.cache s 帮助序列中的每个元素只评估一次,尽管Seq.tail s 仍然需要跳过元素但它不需要重新计算跳过的元素?
    • 您的解决方案,@Tomas Petricek 之一(以及数组版本)需要内存来存储生成的元素。我最初的天真想法是使用序列来实现 Erastothenes 的“零分配内存”版本。
    • 是的,Seq.cache 允许Seq.tail 跳过一个元素,而无需重新计算序列。这是经典的time-space tradeoff
    【解决方案2】:

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

    如果您使用列表,则效率会更高,其中与 x::xs 的模式匹配只需引用下一个 cons 单元格:

    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.tail 大约是O(n)。我相信你的猜测是正确的,递归 Seq.tail 的总开销大约是 O(n^2 / log n) (因为会有大约 n / log n 递归调用。
    【解决方案3】:

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

    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
    

    【讨论】:

      猜你喜欢
      • 2011-12-27
      • 2020-05-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-11-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多