【问题标题】:finding primes very slow in F#在 F# 中查找素数非常慢
【发布时间】:2014-05-20 08:52:02
【问题描述】:

我在 C 语言中使用 Eratosthenes 筛非常轻松地回答了 Project Euler 问题 7,我对此没有任何问题。

我对 F# 还是很陌生,所以我尝试实现相同的技术

let prime_at pos =
  let rec loop f l =
    match f with 
    | x::xs -> loop xs (l |> List.filter(fun i -> i % x <> 0 || i = x))
    | _ -> l

  List.nth (loop [2..pos] [2..pos*pos]) (pos-1)

当 pos

然后我尝试将算法更改为

let isPrime n = n > 1 && seq { for f in [2..n/2] do yield f } |> Seq.forall(fun i -> n % i <> 0)

seq {for i in 2..(10000 * 10000) do if isPrime i then yield i} |> Seq.nth 10000 |> Dump

成功运行但仍需要几分钟。

如果我理解正确,第一个算法是尾部优化的,为什么它会崩溃?以及如何编写运行时间不到 1 分钟的算法(我有一台速度很快的计算机)?

【问题讨论】:

标签: algorithm f# primes


【解决方案1】:

看看你的第一次尝试

let prime_at pos =
  let rec loop f l =
    match f with 
    | x::xs -> loop xs (l |> List.filter(fun i -> i % x <> 0 || i = x))
    | _ -> l

  List.nth (loop [2..pos] [2..pos*pos]) (pos-1)

在每次循环迭代中,您都在迭代并创建一个新列表。这非常慢,因为列表创建非常慢,而且您看不到缓存的任何好处。跳过了几个明显的优化,例如跳过偶数的因子列表。当pos=10 000 你试图创建一个列表,它将占用10 000 * 10 000 * 4 = 400MB 的整数和另一个800MB 的指针(F# 列表是链表)。此外,由于每个列表元素占用的内存非常少,因此可能会产生大量开销,例如 GC 开销。然后在函数中创建一个类似大小的新列表。因此,我并不惊讶这会导致OutOfMemoryException

看第二个例子,

let isPrime n = 
    n > 1 && 
    seq { for f in [2..n/2] do yield f } 
    |> Seq.forall(fun i -> n % i <> 0)

这里的问题非常相似,因为您正在为要测试的每个元素生成巨大的列表。

我在这里https://stackoverflow.com/a/12014908/124259 写了一个相当快的 F# 筛子,它展示了如何更快地做到这一点。

【讨论】:

    【解决方案2】:

    正如 John 已经提到的,您的实现很慢,因为它会生成一些临时数据结构。

    • 在第一种情况下,您正在构建一个列表,该列表需要在内存中完全创建,并且会引入大量开销。

    • 在第二种情况下,您正在构建一个惰性序列,它不消耗内存(因为它是在迭代时构建的),但它仍然引入了降低算法速度的间接性。

    在大多数情况下,在 F# 中,人们倾向于更喜欢可读性,因此使用序列是编写代码的好方法,但在这里您可能更关心性能,所以我会避免使用序列。如果你想保持你的代码结构不变,你可以像这样重写isPrime

    let isPrime n = 
      let rec nonDivisible by =
        if by = 1 then true        // Return 'true' if we reached the end
        elif n%by = 0 then false   // Return 'false' if there is a divisor
        else nonDivisible (by - 1) // Otherwise continue looping
    
      n > 1 && nonDivisible (n/2)
    

    这只是将序列和forall 替换为递归函数nonDivisible,当数字n 不能被2 和n/2 之间的任何数字整除时返回true。该函数首先检查两个终止情况,否则执行递归调用..

    使用原始实现,我可以在 1.5 秒内找到第 1000 个素数,而使用新实现则需要 22 毫秒。在我的机器上使用新实现查找第 10000 个素数需要 3.2 秒。

    【讨论】:

    • 这行得通给我留下了深刻的印象!我了解该算法,但您能否解释一下为什么这比序列快得多?我在某处读到,与 C#(使用状态机)相比,F# 引擎实现 IEnumerable 的效果很差。这可能是为什么?
    • @happygilmore : 当前版本的 F# 还为序列表达式生成状态机;你读到的东西肯定已经过时了。
    • @happygilmore 你的意思是 GordonBGoods 回答的最后一段吗?stackoverflow.com/questions/12014224/…
    • @happygilmore:由于您正在执行 Project Euler,因此对 isPrime 函数的另一个优化是您只需要检查直到目标平方根的数字。如果一个数没有低于或等于其平方根的除数,则该数是素数。这明显低于 n/2。
    猜你喜欢
    • 1970-01-01
    • 2022-01-12
    • 1970-01-01
    • 2021-02-05
    • 2013-06-13
    • 1970-01-01
    • 2020-01-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多