【问题标题】:Getting functional sieve of Eratosthenes fast快速获得 Eratosthenes 的功能筛
【发布时间】:2011-09-22 03:15:16
【问题描述】:

我读过this other post about a F# version of this algorithm。我发现它非常优雅,并尝试将答案的一些想法结合起来。

虽然我对其进行了优化以减少检查(仅检查 6 左右的数字)并省去不必要的缓存,但它仍然非常缓慢。计算第 10,000th 个素数已经花费了 5 多分钟。使用命令式方法,我可以在不多的时间内测试所有 31 位整数。

所以我的问题是我是否遗漏了一些让这一切变得如此缓慢的东西。例如在another post 中,有人推测LazyList 可能会使用锁定。有人有想法吗?

由于 StackOverflow 的规则规定不要发布新问题作为答案,我觉得我必须为此开始一个新话题。

代码如下:

#r "FSharp.PowerPack.dll"

open Microsoft.FSharp.Collections

let squareLimit = System.Int32.MaxValue |> float32 |> sqrt |> int

let around6 = LazyList.unfold (fun (candidate, (plus, next)) -> 
        if candidate > System.Int32.MaxValue - plus then
            None
        else
            Some(candidate, (candidate + plus, (next, plus)))
    ) (5, (2, 4))

let (|SeqCons|SeqNil|) s =
    if Seq.isEmpty s then SeqNil
    else SeqCons(Seq.head s, Seq.skip 1 s)

let rec lazyDifference l1 l2 =
    if Seq.isEmpty l2 then l1 else
    match l1, l2 with
    | LazyList.Cons(x, xs), SeqCons(y, ys) ->
        if x < y then
            LazyList.consDelayed x (fun () -> lazyDifference xs l2)
        elif x = y then
            lazyDifference xs ys
        else
            lazyDifference l1 ys
    | _ -> LazyList.empty

let lazyPrimes =
    let rec loop = function
        | LazyList.Cons(p, xs) as ll ->
            if p > squareLimit then
                ll
            else
                let increment = p <<< 1
                let square = p * p
                let remaining = lazyDifference xs {square..increment..System.Int32.MaxValue}
                LazyList.consDelayed p (fun () -> loop remaining)
        | _ -> LazyList.empty
    loop (LazyList.cons 2 (LazyList.cons 3 around6))

【问题讨论】:

  • 缓慢是在你的(|SeqCons|SeqNil|) 活动模式中,大约需要 O(n^2) 时间。我认为没有办法对序列进行模式匹配,因此您最好将其转换为 LazyList 。在这里查看 brian 的精彩回答:stackoverflow.com/questions/1306140/…
  • 您可能会对此感兴趣。 stackoverflow.com/questions/4629734/…
  • 客观上,这是一个未解决的问题。没有已知的方法来实现具有竞争力的高效纯 Eratosthenes 筛。您可以对其进行一些优化,但您永远不会接近命令式解决方案的性能,因此这是一项学术练习。如果你想编写快速的代码来解决这些问题,你必须接受杂质。此外,我相信纯和不纯之间的性能差距永远不会缩小,因为纯准确地抽象出您编写快速代码所需的信息。
  • @JonHarrop 确实working with lists有区别,类似于整数排序和比较排序的区别;但是纯代码的复杂性可以显着降低到接近最佳值(请参阅我在此线程中的回答)。但是working with immutable arrays 没有什么能阻止智能实现使用破坏性更新,从而实现命令式实现的真正最佳复杂性(理论上)。
  • @WillNess:我同意一个足够聪明的编译器可以神奇地将纯源代码优化为理论上具有竞争效率的不纯实现,但我们今天离拥有这种复杂程度的编译器还差得很远,我做到了不相信它会发生。这只是重新审视了 Lisp 的“足够聪明的编译器”神话。在实践中,构建能够进行更多优化的编译器会降低结果代码性能的可预测性,以至于它实际上毫无用处(只需看看斯大林方案编译器)。

标签: performance f# lazy-evaluation primes sieve-of-eratosthenes


【解决方案1】:

如果您在任何地方调用Seq.skip,那么您有大约 99% 的机会使用 O(N^2) 算法。对于几乎所有涉及序列的优雅函数式惰性 Project Euler 解决方案,您都希望使用 LazyList,而不是 Seq。 (有关更多讨论,请参阅 Juliet 的评论链接。)

【讨论】:

  • 有趣,这肯定可以解释缓慢的原因。但他的问题是我之前也使用了LazyList 而不是Seq。但是由于LazyList 使用缓存,我的测试总是耗尽内存(大约 1.5 TB)。这就是我切换到Seq 的原因。我可以禁用 LazyList 的缓存吗?
  • 不,但是如果您不再需要 LazyList 的前面,您可以丢弃它。只要确保你没有保存对列表前面的任何引用,你可以“尾随”到它,让前面在你使用它时得到 GC。
  • 嗨,Brian,我认为LazyList 本身就是缓存,这是错误的吗?因此,即使 I 在某些时候不保留对 head 的值的引用,由于缓存,list 也必须。因此 1.5 TB。我有什么问题吗?
  • 是的。它是一个单链表,在你加入并丢弃头部之后没有任何东西可以缓存。
【解决方案2】:

即使您成功解决了奇怪的二次 F# 序列设计问题,仍有一些算法改进。你在这里以(...((x-a)-b)-...) 的方式工作。 xaround6 越来越深,但它是最频繁产生的序列。 Transform 将其添加到 (x-(a+b+...)) 方案中——或者甚至在那里使用树结构——以在时间上获得改进复杂性(抱歉,该页面在 Haskell 中)。这实际上非常接近命令式筛选的复杂性,尽管仍然比基线 C++ 代码慢。

将本地empirical orders of growth 测量为O(n^a) &lt;--&gt; a = log(t_2/t_1) / log(n_2/n_1)n 产生的素数中),理想的n log(n) log(log(n)) 转化为O(n^1.12) .. O(n^1.085)n=10^5..10^7 范围内的行为。一个简单的 C++ 基线imperative code 实现了O(n^1.45 .. 1.18 .. 1.14)tree-merging code 以及基于优先级队列的代码都或多或少地表现出稳定的O(n^1.20) 行为。当然,C++ 的速度要快 ~5020..15 倍,但这主要只是一个“常数因素”。 :)

【讨论】:

  • 是的,该死的这些常数因素。 ;-) 不幸的是,虽然我真的很喜欢函数式,但我很难阅读 Haskell。
  • @primfaktor 看看here,看看它是否有助于阅读 Haskell。如果你知道 Prolog,另请参阅我的另一个 recent answer
  • @primfaktor C++ 实际上快了 20..15 倍,我刚刚更正了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-05
  • 1970-01-01
  • 1970-01-01
  • 2013-05-28
相关资源
最近更新 更多