F#:低效的序列处理

F#: inefficient sequence processing

我有以下代码在 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 |> Seq.skip 1000;; 对我来说需要将近一分钟,尽管 primes 1000000 本身自然非常快,因为它只是一个序列.

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

如果这是 Python 并且序列对象是一个生成器,那么确保此时不会发生任何昂贵的事情将是微不足道的:只是序列中的 yield,我们已经很便宜了丢弃了它的第一个元素。

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

如何通过防止不必要的 duplications/recomputations 来加快此实施?理想情况下,无论 n 有多大,primesLessThan n |> Seq.skip 1000 都会花费相同的时间。

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

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

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

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

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

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

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

我实现了一个与 Seq 非常相似的 LazyList,它允许我计算评估的数量:

对于 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 运行 大致相似,但由于更紧凑的内存布局,GC 指标更适合 Array(Array 的 10 cc0 集合与 16列表的 cc0 集合)。 Seq 的 GC 指标更差,因为它强制执行 4 次 cc1 回收。

筛选算法的可变实现具有更好的内存和性能指标。