【问题标题】:F#: inefficient sequence processingF#:低效的序列处理
【发布时间】:2018-03-11 10:44:06
【问题描述】:

我有以下代码在 F# 中执行埃拉托色尼筛法:

let sieveOutPrime p numbers =
   numbers
   |> Seq.filter (fun n -> n % p <> 0)

let primesLessThan n =
    let removeFirstPrime = function
       | s when Seq.isEmpty s -> None
       | s -> Some(Seq.head s, sieveOutPrime (Seq.head s) (Seq.tail s))

    let remainingPrimes =
       seq {3..2..n}
       |> Seq.unfold removeFirstPrime

    seq { yield 2; yield! remainingPrimes }

primesLessThan 的输入远程很大时,这会非常慢:primes 1000000 |&gt; Seq.skip 1000;; 对我来说需要将近一分钟,尽管primes 1000000 本身自然非常快,因为它只是一个序列。

我玩了一些,我认为罪魁祸首一定是Seq.tail(在我的removeFirstPrime)正在做一些密集的事情。 According to the docs,它正在生成一个全新的序列,我可以想象它很慢。

如果这是 Python 并且序列对象是一个生成器,那么确保此时不会发生任何代价高昂的事情将是微不足道的:只需来自序列的 yield,我们已经廉价地删除了它的第一个元素。

LazyList 在 F# doesn't seem 中具有 unfold 方法(或者,就此而言,filter 方法);否则我认为LazyList 会是我想要的。

如何通过防止不必要的重复/重新计算来加快实现速度?理想情况下,无论n 有多大,primesLessThan n |&gt; Seq.skip 1000 都会花费相同的时间。

【问题讨论】:

  • it is。我用 n=10000、20000 或 100000 尝试了你的代码,观察到相同的运行时间。
  • 但更改 primesLessThan n |&gt; Seq.skip m |&gt; Seq.take 10 中的 m 确实会改变时间并显示它运行在大约 ~m^3 empirically,而不是理论上的 ~m^2 用于您的算法(产生 m 个素数)。当然,立方时间不是野餐。 :)(也是二次的)
  • 对于 a 解决方案,请参阅RosettaCode entry which runs at m^1.4 经验性地,在生产 的测试范围内m = 100K..200K 个素数。它使用不同的算法以及不同的定制类型。 (免责声明:我不是作者)。
  • @scrwtp 已经链接到一个这样说的答案,但我会在这里重复给任何发现这个问题的人:不要在递归代码中使用 Seq.tail。你可能认为因为 List.tail 是 O(1),所以 Seq.tail 也是 O(1)。它不是。 Seq.tail 是 O(N),会导致 O(N) 算法变为 O(N^2),或者 O(N^2) 算法变为 O(N^3) 等等。 (我看到你已经弄清楚了,但我想重复一遍,因为这是我在 seqs 中看到的第二个最常见的错误。第一个最常见的错误是忘记它们是懒惰的)。
  • @rmunn 事实上,我认为 Seq.tail 是 O(1) 的原因是因为 Python 中的类似概念是 O(1) - 但重点是,谢谢。

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


【解决方案1】:

递归解决方案和序列不能很好地结合在一起(比较答案here,这与您使用的模式非常相似)。您可能想检查生成的代码,但我只是认为这是一个经验法则。

LazyList(在 FSharpX 中定义)当然带有 unfold 并定义了过滤器,如果没有,那就太奇怪了。通常在 F# 代码中,这种功能是在单独的模块中提供的,而不是作为类型本身的实例成员提供的,这种约定似乎确实使大多数文档系统感到困惑。

【讨论】:

    【解决方案2】:

    您可能知道Seq 是一个惰性求值的集合。 Sieve 算法是关于从序列中过滤掉非素数,这样您就不必再考虑它们了。

    但是,当您将 Sieve 与延迟评估的集合结合使用时,您最终会一遍又一遍地过滤相同的非素数。

    如果您从Seq 切换到ArrayList,您会看到更好的性能,因为这些集合的非惰性方面意味着您只过滤非素数一次。

    提高代码性能的一种方法是引入缓存。

    let removeFirstPrime s =
       let s = s |> Seq.cache 
       match s with
       | s when Seq.isEmpty s -> None
       | s -> Some(Seq.head s, sieveOutPrime (Seq.head s) (Seq.tail s))
    

    我实现了一个LazyList,它的工作原理很像Seq,它允许我计算评估的数量:

    对于 2000 以内的所有素数。

    • 没有缓存:14753706 次评估
    • 使用缓存:97260 次评估

    当然,如果您真的需要性能,您可以使用可变数组实现。

    PS。性能指标

    Running 'seq' ...
      it took 271 ms with cc (16, 4, 0), the result is: 1013507
    Running 'list' ...
      it took 14 ms with cc (16, 0, 0), the result is: 1013507
    Running 'array' ...
      it took 14 ms with cc (10, 0, 0), the result is: 1013507
    Running 'mutable' ...
      it took 0 ms with cc (0, 0, 0), the result is: 1013507
    

    这是Seq,带有缓存。 F# 中的 Seq 具有相当高的开销,Seq 有一些有趣的惰性替代品,例如 Nessos

    ListArray 运行大致相似,但由于内存布局更紧凑,Array 的 GC 指标更好(Array 为 10 个 cc0 集合,List 为 16 个 cc0 集合)。 Seq 的 GC 指标更差,因为它强制进行 4 次 cc1 收集。

    筛算法的可变实现在很大程度上具有更好的内存和性能指标。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-10-22
      • 2015-05-18
      • 2018-05-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多