F# 惯用性能
F# Idiomatic Performance
我正在使用 Exercism to learn F#. The Nth Prime challenge was to build a Sieve of Eratosthenes。单元测试要求您搜索第 1,001 个素数,即 104,743。
我修改了我从 F# For Fun and Profit 中记得的代码片段以批量工作(需要 10k 个素数,而不是 25 个)并将其与我自己的命令式版本进行比较。存在显着的性能差异:
(BenchmarkDotNet v0.11.2)
有没有一种有效的方法可以惯用地做到这一点?我喜欢 F#。我喜欢使用 F# 库节省的时间。但有时我看不到有效的惯用路线。
这里是惯用代码:
// we only need to check numbers ending in 1, 3, 7, 9 for prime
let getCandidates seed =
let nextTen seed ten =
let x = (seed) + (ten * 10)
[x + 1; x + 3; x + 7; x + 9]
let candidates = [for x in 0..9 do yield! nextTen seed x ]
match candidates with
| 1::xs -> xs //skip 1 for candidates
| _ -> candidates
let filterCandidates (primes:int list) (candidates:int list): int list =
let isComposite candidate =
primes |> List.exists (fun p -> candidate % p = 0 )
candidates |> List.filter (fun c -> not (isComposite c))
let prime nth : int option =
match nth with
| 0 -> None
| 1 -> Some 2
| _ ->
let rec sieve seed primes candidates =
match candidates with
| [] -> getCandidates seed |> filterCandidates primes |> sieve (seed + 100) primes //get candidates from next hunderd
| p::_ when primes.Length = nth - 2 -> p //value found; nth - 2 because p and 2 are not in primes list
| p::xs when (p * p) < (seed + 100) -> //any composite of this prime will not be found until after p^2
sieve seed (p::primes) [for x in xs do if (x % p) > 0 then yield x]
| p::xs ->
sieve seed (p::primes) xs
Some (sieve 0 [3; 5] [])
命令如下:
type prime =
struct
val BaseNumber: int
val mutable NextMultiple: int
new (baseNumber) = {BaseNumber = baseNumber; NextMultiple = (baseNumber * baseNumber)}
//next multiple that is odd; (odd plus odd) is even plus odd is odd
member this.incrMultiple() = this.NextMultiple <- (this.BaseNumber * 2) + this.NextMultiple; this
end
let prime nth : int option =
match nth with
| 0 -> None
| 1 -> Some 2
| _ ->
let nth' = nth - 1 //not including 2, the first prime
let primes = Array.zeroCreate<prime>(nth')
let mutable primeCount = 0
let mutable candidate = 3
let mutable isComposite = false
while primeCount < nth' do
for i = 0 to primeCount - 1 do
if primes.[i].NextMultiple = candidate then
isComposite <- true
primes.[i] <- primes.[i].incrMultiple()
if isComposite = false then
primes.[primeCount] <- new prime(candidate)
primeCount <- primeCount + 1
isComposite <- false
candidate <- candidate + 2
Some primes.[nth' - 1].BaseNumber
乍一看,您不是在比较相同的概念。当然,我不是在谈论功能与命令,而是算法本身背后的概念。
您的 wiki 参考资料说得最好:
This is the sieve's key distinction from using trial division to sequentially test each candidate number for divisibility by each prime.
换句话说,埃拉托色尼筛法的强大之处在于不使用试除法。另一个 wiki ref:
Trial division is the most laborious but easiest to understand of the integer factorization algorithms.
这实际上就是您在过滤器中所做的。
let isComposite candidate =
primes |> List.exists (fun p -> candidate % p = 0 )
所以一般来说,当使用函数式习语时,您可能期望比使用命令式模型时慢一点,因为您必须创建新对象,这比修改现有对象花费的时间要长得多。
对于这个问题,特别是当使用 F# 列表时,与使用数组相比,您每次都需要迭代素数列表这一事实是一种性能损失。您还应该注意,您不需要单独生成候选列表,您只需循环并动态添加 2 即可。也就是说最大的性能胜利可能是使用变异来存储你的 nextNumber
.
type prime = {BaseNumber: int; mutable NextNumber: int}
let isComposite (primes:prime list) candidate =
let rec inner primes candidate =
match primes with
| [] -> false
| p::ps ->
match p.NextNumber = candidate with
| true -> p.NextNumber <- p.NextNumber + p.BaseNumber*2
inner ps candidate |> ignore
true
| false -> inner ps candidate
inner primes candidate
let prime nth: int option =
match nth with
| 0 -> None
| 1 -> Some 2
| _ ->
let rec findPrime (primes: prime list) (candidate: int) (n: int) =
match nth - n with
| 1 -> primes
| _ -> let isC = isComposite primes candidate
if (not isC) then
findPrime ({BaseNumber = candidate; NextNumber = candidate*candidate}::primes) (candidate + 2) (n+1)
else
findPrime primes (candidate + 2) n
let p = findPrime [{BaseNumber = 3; NextNumber = 9};{BaseNumber = 5; NextNumber = 25}] 7 2
|> List.head
Some(p.BaseNumber)
运行 通过 #time
,我得到大约 500 毫秒到 运行 prime 10001
。相比之下,您的 "imperative" 代码大约需要 250 毫秒,而您的 "idomatic" 代码大约需要 1300 毫秒。
我正在使用 Exercism to learn F#. The Nth Prime challenge was to build a Sieve of Eratosthenes。单元测试要求您搜索第 1,001 个素数,即 104,743。
我修改了我从 F# For Fun and Profit 中记得的代码片段以批量工作(需要 10k 个素数,而不是 25 个)并将其与我自己的命令式版本进行比较。存在显着的性能差异:
有没有一种有效的方法可以惯用地做到这一点?我喜欢 F#。我喜欢使用 F# 库节省的时间。但有时我看不到有效的惯用路线。
这里是惯用代码:
// we only need to check numbers ending in 1, 3, 7, 9 for prime
let getCandidates seed =
let nextTen seed ten =
let x = (seed) + (ten * 10)
[x + 1; x + 3; x + 7; x + 9]
let candidates = [for x in 0..9 do yield! nextTen seed x ]
match candidates with
| 1::xs -> xs //skip 1 for candidates
| _ -> candidates
let filterCandidates (primes:int list) (candidates:int list): int list =
let isComposite candidate =
primes |> List.exists (fun p -> candidate % p = 0 )
candidates |> List.filter (fun c -> not (isComposite c))
let prime nth : int option =
match nth with
| 0 -> None
| 1 -> Some 2
| _ ->
let rec sieve seed primes candidates =
match candidates with
| [] -> getCandidates seed |> filterCandidates primes |> sieve (seed + 100) primes //get candidates from next hunderd
| p::_ when primes.Length = nth - 2 -> p //value found; nth - 2 because p and 2 are not in primes list
| p::xs when (p * p) < (seed + 100) -> //any composite of this prime will not be found until after p^2
sieve seed (p::primes) [for x in xs do if (x % p) > 0 then yield x]
| p::xs ->
sieve seed (p::primes) xs
Some (sieve 0 [3; 5] [])
命令如下:
type prime =
struct
val BaseNumber: int
val mutable NextMultiple: int
new (baseNumber) = {BaseNumber = baseNumber; NextMultiple = (baseNumber * baseNumber)}
//next multiple that is odd; (odd plus odd) is even plus odd is odd
member this.incrMultiple() = this.NextMultiple <- (this.BaseNumber * 2) + this.NextMultiple; this
end
let prime nth : int option =
match nth with
| 0 -> None
| 1 -> Some 2
| _ ->
let nth' = nth - 1 //not including 2, the first prime
let primes = Array.zeroCreate<prime>(nth')
let mutable primeCount = 0
let mutable candidate = 3
let mutable isComposite = false
while primeCount < nth' do
for i = 0 to primeCount - 1 do
if primes.[i].NextMultiple = candidate then
isComposite <- true
primes.[i] <- primes.[i].incrMultiple()
if isComposite = false then
primes.[primeCount] <- new prime(candidate)
primeCount <- primeCount + 1
isComposite <- false
candidate <- candidate + 2
Some primes.[nth' - 1].BaseNumber
乍一看,您不是在比较相同的概念。当然,我不是在谈论功能与命令,而是算法本身背后的概念。
您的 wiki 参考资料说得最好:
This is the sieve's key distinction from using trial division to sequentially test each candidate number for divisibility by each prime.
换句话说,埃拉托色尼筛法的强大之处在于不使用试除法。另一个 wiki ref:
Trial division is the most laborious but easiest to understand of the integer factorization algorithms.
这实际上就是您在过滤器中所做的。
let isComposite candidate =
primes |> List.exists (fun p -> candidate % p = 0 )
所以一般来说,当使用函数式习语时,您可能期望比使用命令式模型时慢一点,因为您必须创建新对象,这比修改现有对象花费的时间要长得多。
对于这个问题,特别是当使用 F# 列表时,与使用数组相比,您每次都需要迭代素数列表这一事实是一种性能损失。您还应该注意,您不需要单独生成候选列表,您只需循环并动态添加 2 即可。也就是说最大的性能胜利可能是使用变异来存储你的 nextNumber
.
type prime = {BaseNumber: int; mutable NextNumber: int}
let isComposite (primes:prime list) candidate =
let rec inner primes candidate =
match primes with
| [] -> false
| p::ps ->
match p.NextNumber = candidate with
| true -> p.NextNumber <- p.NextNumber + p.BaseNumber*2
inner ps candidate |> ignore
true
| false -> inner ps candidate
inner primes candidate
let prime nth: int option =
match nth with
| 0 -> None
| 1 -> Some 2
| _ ->
let rec findPrime (primes: prime list) (candidate: int) (n: int) =
match nth - n with
| 1 -> primes
| _ -> let isC = isComposite primes candidate
if (not isC) then
findPrime ({BaseNumber = candidate; NextNumber = candidate*candidate}::primes) (candidate + 2) (n+1)
else
findPrime primes (candidate + 2) n
let p = findPrime [{BaseNumber = 3; NextNumber = 9};{BaseNumber = 5; NextNumber = 25}] 7 2
|> List.head
Some(p.BaseNumber)
运行 通过 #time
,我得到大约 500 毫秒到 运行 prime 10001
。相比之下,您的 "imperative" 代码大约需要 250 毫秒,而您的 "idomatic" 代码大约需要 1300 毫秒。