F# 惯用性能

Posted

技术标签:

【中文标题】F# 惯用性能【英文标题】:F# Idiomatic Performance 【发布时间】:2019-04-20 18:43:37 【问题描述】:

我正在使用Exercism to learn F#。 Nth Prime 挑战是建立一个Sieve of Eratosthenes。单元测试让您搜索第 1,001 个素数,即 104,743。

我修改了我记得的 F# For Fun and Profit 的代码 sn-p 以批量工作(需要 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

【问题讨论】:

@EricP:为了接近惯用的函数式解决方案性能方面,您可能需要查看来自this SO answer 的primes F# 序列定义。我相信primes |&gt; Seq.item 10001 可能会严重击败您提出的命令式解决方案。然后,从那里挑选惰性和记忆习语的实际应用,您可能会开始对函数式代码效率感觉更好。 【参考方案1】:

因此,一般而言,使用函数式惯用语时,您可能希望比使用命令式模型时要慢一些,因为您必须创建新对象,这比修改现有对象花费的时间要长得多。

对于这个问题,特别是在使用 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。相比之下,您的“命令式”代码大约需要 250 毫秒,而您的“惯用”代码大约需要 1300 毫秒。

【讨论】:

【参考方案2】:

乍一看,您并不是在比较相同的概念。当然,我不是在谈论函数式与命令式,而是算法本身背后的概念。

你的维基参考说得最好:

这是筛子与使用试除法顺序测试每个候选数是否可被每个素数整除的关键区别。

换句话说,埃拉托色尼筛法的威力在于不使用试除法。另一个wiki ref:

试除法是整数分解算法中最费力但最容易理解的算法。

这实际上就是您在过滤器中所做的事情。

let isComposite candidate =  
    primes |> List.exists (fun p -> candidate % p = 0 ) 

【讨论】:

我明白了。此外,公平地说,Scott Wlaschin 说这是“素数筛的粗略实现”,它是要找到前 25 个素数,而不是前 10k。但问题仍然存在,什么是有效的、惯用的实现?我的大脑直接进入命令状态,我正在努力学习一种新方法。 @EricP 对于 F# 的惯用方式是混合命令式和声明式代码。不要将 F# 与 Haskell 混淆。

以上是关于F# 惯用性能的主要内容,如果未能解决你的问题,请参考以下文章

附加到简短的 python 列表的惯用语法是啥?

F# 惯用的 async while 循环累加转换

是否值得惯用编程?一个 ES6 示例 [关闭]

这是 F# 中 Option 惯用的用法吗?

F# - 将 100 个对象创建到一个列表中 - 最实用和惯用的方式

派生类型的模式匹配对于 F# 来说是惯用的吗?