提高序列的性能
Improving the performance of sequence
我正在实现埃拉托色尼筛法的两个版本,第一个是命令式的:
let primesUntilArray n =
let isqrt x =
let mutable root = 0UL
let mutable p = 1UL <<< 31
while p > 0UL do
while x < (root + p) * (root + p) do
p <- p >>> 1
root <- root + p
p <- p >>> 1
root
let isPrime = Array.create (int <| n + 1) true
let bound = int <| isqrt (uint64 n)
let mutable i = 2
while i <= bound do
if isPrime.[i] then
let mutable j = i * i
while j <= n do
isPrime.[j] <- false
j <- j + i
i <- i + 1
let mutable primes = []
let mutable i = 2
while i <= n do
if isPrime.[i] then
primes <- i :: primes
i <- i + 1
List.rev primes
而第二个功能正常(使用 F# 的序列):
let primesUntilSequence n =
let rec pickPrimes s =
let sieveWith p =
Seq.filter (fun n -> n < p * p || n % p <> 0)
let p = Seq.head s
seq {
yield p
yield! pickPrimes (sieveWith p (Seq.tail s))
}
Seq.initInfinite (fun i -> i + 2)
|> pickPrimes
|> Seq.takeWhile (fun p -> p <= n)
|> Seq.toList
两者的时间复杂度都在O(n log log n)
左右,但是使用sequence的版本性能很差,例如
> primesUntilArray 9000 |> List.length |> printfn "%d";;
1117
Real: 00:00:00.004, CPU: 00:00:00.000, GC Gen0: 1, Gen1: 0, Gen2: 0
val it: unit = ()
> primesUntilSequence 9000 |> List.length |> printfn "%d";;
1117
Real: 00:00:15.388, CPU: 00:00:15.375, GC Gen0: 592, Gen1: 64, Gen2: 0
val it: unit = ()
这意味着直到 9000 时生成素数的速度要慢大约 4000 倍。我怎样才能提高第二个素数的性能?
谢谢大家。
以下是@Tomas Petricek
解决方案的略微修改版本
let primesUntilList n =
let rec pickPrimes ps xs =
let sieveWith p =
List.filter (fun n -> n < p * p || n % p <> 0)
match xs with
| p :: xs' ->
if p * p > n then
(List.rev xs) @ ps
else
pickPrimes (p :: ps) (sieveWith p xs')
| _ -> ps
List.rev <| pickPrimes [] [ 2..n ]
这个基于列表的版本仍然比使用数组的效率低两倍,但比我最初的基于序列的版本要好得多。
基于序列的版本的问题是使用 Seq.head
和 Seq.tail
递归地解构序列是非常低效的。 Seq.tail
返回的序列迭代原始序列,但跳过第一个元素。这意味着通过递归地应用 Seq.tail
,您将创建越来越多的需要迭代的序列(我猜这是 O(N^2))。
如果你使用一个列表,这会变得更有效率,其中针对 x::xs
的模式匹配只是引用下一个缺点单元格:
let primesUntilList n =
let rec pickPrimes s =
let sieveWith p =
List.filter (fun n -> n < p * p || n % p <> 0)
match s with
| [] -> []
| p::ps -> p :: pickPrimes (sieveWith p ps)
[ 2 .. n ] |> pickPrimes
这仍然比基于数组的版本效率低(我认为这是可以预料的),但它的性能不如基于序列的版本。
Seq.cache
当您想多次评估一个序列时很有用(这实际上是 Seq.tail
所做的)。您可以像这样将其放入原始代码中:
let primesUntilSequence n =
let rec pickPrimes s =
let sieveWith p =
Seq.filter (fun n -> n < p * p || n % p <> 0)
let s = Seq.cache s // *** cache the sequence ***
let p = Seq.head s
seq {
yield p
yield! pickPrimes (sieveWith p (Seq.tail s))
}
Seq.initInfinite (fun i -> i + 2)
|> pickPrimes
|> Seq.takeWhile (fun p -> p <= n)
|> Seq.toList
在我的盒子上,primesUntilSequence 9000
现在运行大约四分之一秒。
在处理序列性能问题时,在序列表达式中命令式地处理它们会很有帮助。简而言之,带上你自己的枚举器。
与 array
和 list
方法相比,这仍然非常低效。至少您可以完全控制嵌套序列所需的资源分配。
type IEnumerator<'a> =
System.Collections.Generic.IEnumerator<'a>
let primesUntilIterator n =
let sieveWith p (en : IEnumerator<_>) = seq{
while en.MoveNext() do
let n = en.Current
if n < p * p || n % p <> 0 then
yield n }
let rec pickPrimes (s : seq<_>) = seq{
use en = s.GetEnumerator()
if en.MoveNext() then
let p = en.Current
yield p
yield! pickPrimes (sieveWith p en) }
{ 2..n } |> pickPrimes
我正在实现埃拉托色尼筛法的两个版本,第一个是命令式的:
let primesUntilArray n =
let isqrt x =
let mutable root = 0UL
let mutable p = 1UL <<< 31
while p > 0UL do
while x < (root + p) * (root + p) do
p <- p >>> 1
root <- root + p
p <- p >>> 1
root
let isPrime = Array.create (int <| n + 1) true
let bound = int <| isqrt (uint64 n)
let mutable i = 2
while i <= bound do
if isPrime.[i] then
let mutable j = i * i
while j <= n do
isPrime.[j] <- false
j <- j + i
i <- i + 1
let mutable primes = []
let mutable i = 2
while i <= n do
if isPrime.[i] then
primes <- i :: primes
i <- i + 1
List.rev primes
而第二个功能正常(使用 F# 的序列):
let primesUntilSequence n =
let rec pickPrimes s =
let sieveWith p =
Seq.filter (fun n -> n < p * p || n % p <> 0)
let p = Seq.head s
seq {
yield p
yield! pickPrimes (sieveWith p (Seq.tail s))
}
Seq.initInfinite (fun i -> i + 2)
|> pickPrimes
|> Seq.takeWhile (fun p -> p <= n)
|> Seq.toList
两者的时间复杂度都在O(n log log n)
左右,但是使用sequence的版本性能很差,例如
> primesUntilArray 9000 |> List.length |> printfn "%d";;
1117
Real: 00:00:00.004, CPU: 00:00:00.000, GC Gen0: 1, Gen1: 0, Gen2: 0
val it: unit = ()
> primesUntilSequence 9000 |> List.length |> printfn "%d";;
1117
Real: 00:00:15.388, CPU: 00:00:15.375, GC Gen0: 592, Gen1: 64, Gen2: 0
val it: unit = ()
这意味着直到 9000 时生成素数的速度要慢大约 4000 倍。我怎样才能提高第二个素数的性能?
谢谢大家。 以下是@Tomas Petricek
解决方案的略微修改版本let primesUntilList n =
let rec pickPrimes ps xs =
let sieveWith p =
List.filter (fun n -> n < p * p || n % p <> 0)
match xs with
| p :: xs' ->
if p * p > n then
(List.rev xs) @ ps
else
pickPrimes (p :: ps) (sieveWith p xs')
| _ -> ps
List.rev <| pickPrimes [] [ 2..n ]
这个基于列表的版本仍然比使用数组的效率低两倍,但比我最初的基于序列的版本要好得多。
基于序列的版本的问题是使用 Seq.head
和 Seq.tail
递归地解构序列是非常低效的。 Seq.tail
返回的序列迭代原始序列,但跳过第一个元素。这意味着通过递归地应用 Seq.tail
,您将创建越来越多的需要迭代的序列(我猜这是 O(N^2))。
如果你使用一个列表,这会变得更有效率,其中针对 x::xs
的模式匹配只是引用下一个缺点单元格:
let primesUntilList n =
let rec pickPrimes s =
let sieveWith p =
List.filter (fun n -> n < p * p || n % p <> 0)
match s with
| [] -> []
| p::ps -> p :: pickPrimes (sieveWith p ps)
[ 2 .. n ] |> pickPrimes
这仍然比基于数组的版本效率低(我认为这是可以预料的),但它的性能不如基于序列的版本。
Seq.cache
当您想多次评估一个序列时很有用(这实际上是 Seq.tail
所做的)。您可以像这样将其放入原始代码中:
let primesUntilSequence n =
let rec pickPrimes s =
let sieveWith p =
Seq.filter (fun n -> n < p * p || n % p <> 0)
let s = Seq.cache s // *** cache the sequence ***
let p = Seq.head s
seq {
yield p
yield! pickPrimes (sieveWith p (Seq.tail s))
}
Seq.initInfinite (fun i -> i + 2)
|> pickPrimes
|> Seq.takeWhile (fun p -> p <= n)
|> Seq.toList
在我的盒子上,primesUntilSequence 9000
现在运行大约四分之一秒。
在处理序列性能问题时,在序列表达式中命令式地处理它们会很有帮助。简而言之,带上你自己的枚举器。
与 array
和 list
方法相比,这仍然非常低效。至少您可以完全控制嵌套序列所需的资源分配。
type IEnumerator<'a> =
System.Collections.Generic.IEnumerator<'a>
let primesUntilIterator n =
let sieveWith p (en : IEnumerator<_>) = seq{
while en.MoveNext() do
let n = en.Current
if n < p * p || n % p <> 0 then
yield n }
let rec pickPrimes (s : seq<_>) = seq{
use en = s.GetEnumerator()
if en.MoveNext() then
let p = en.Current
yield p
yield! pickPrimes (sieveWith p en) }
{ 2..n } |> pickPrimes