在F#中找到素数很慢

时间:2014-05-20 08:52:02

标签: algorithm f# primes

我在C中使用了Eratosthenes的Sieve非常容易地回答了Project Euler Question 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&lt; 1000,但会因超出内存异常而在10000崩溃

然后我尝试将算法更改为

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分钟的算法(我有一台快速计算机)?

2 个答案:

答案 0 :(得分:5)

看着你的第一次尝试

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)

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

我在这里写了一个非常快速的F#筛[{3}},它展示了如何更快地完成这项工作。

答案 1 :(得分:3)

正如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,当true号码不能被2和{{1之间的任何数字整除“时,它返回n }}。该函数首先检查两个终止案例,否则执行递归调用..

通过最初的实现,我能够在1.5秒内找到第1000个素数,并且使用新的素数,需要22ms。使用新实现找到第10000个素数在我的机器上需要3.2秒。