簡體   English   中英

尾遞歸和Seq庫之間的F#性能差異

[英]F# performance difference between tail recursion and Seq library

我在F#中有這個代碼,它找到了最小的正數,它可以被1到20之間的所有數字整除。它需要10秒才能完成。

let isDivisableByAll num (divisors: int[]) = Array.forall (fun div -> num % div = 0) divisors

let minNumDividedBy (divisors: int[]) =  
    let rec minNumDividedByAll stopAt acc = 
        if acc >= stopAt then 0
        else if isDivisableByAll acc divisors then acc
        else minNumDividedByAll stopAt (acc + 1)
    minNumDividedByAll 400000000 1

minNumDividedBy [|1..20|]

所以,我認為我可以使它更優雅,因為我更喜歡更少的代碼並寫下以下內容。

let answer = { 1..400000000 } 
            |> Seq.tryFind (fun el -> isDivisableByAll el [|1..20|])

花了10分鍾! 我無法解釋巨大的差異,因為序列是懶惰的。 為了調查,我寫了一個命令式循環。

let mutable i = 1
while i < 232792561 do
    if isDivisableByAll i [|1..20|] then
        printfn "%d" i
    i <- i + 1

花了8分鍾。 因此,它也不是序列的錯,對吧? 那么,為什么初始函數如此之快? 由於尾遞歸,它不能避免堆積堆棧,可以嗎? 因為我不希望有相當多的堆棧(如果有的話),也可以在慢速示例中構建。

這對我來說沒有多大意義,有人能告訴我嗎?

謝謝。

如果我理解正確,你試圖找到1到400000000(含)之間的數字可以被1到20之間的所有數字整除。我制作了我自己的原始版本:

let factors = Array.rev [| 2 .. 20 |]

let divisible f n =
    Array.forall (fun x -> n % x = 0) f

let solution () =
    {1 .. 400000000}
    |> Seq.filter (divisible factors)
    |> Seq.length

這個解決方案在我測試的地方運行需要90多秒。 但我開始意識到它是Euler第5號問題的一個變體,我們知道2520是第一個可被1到10所有數字整除的數字。利用這個事實,我們可以創建一個2520的倍數序列,僅測試11到19之間的數字,因為保證倍數可以被1到10和20的所有數字整除:

let factors = Array.rev [| 11 .. 19 |]

let divisible f n =
    Array.forall (fun x -> n % x = 0) f

let solution () =
    Seq.initInfinite (fun i -> (i + 1) * 2520)
    |> Seq.takeWhile (fun i -> i <= 400000000)
    |> Seq.filter (divisible factors)
    |> Seq.length

該解決方案需要0.191秒。

如果您不了解歐拉問題編號5,您甚至可以通過算法計算具有給定起始值的倍數的元素的序列。 我們為算法提供了一系列數字,這些數字可以從2到n - 1的所有數字整除,它計算第一個可被2到n的所有數字整除的數字。 這是迭代直到我們有一個第一個數的倍數序列可被我們想要的所有因素整除:

let narrowDown m n s =
    (s, {m .. n})
    ||> Seq.fold (fun a i ->
            let j = Seq.find (fun x -> x % i = 0) a
            Seq.initInfinite (fun i -> (i + 1) * j))

let solution () =
    Seq.initInfinite (fun i -> i + 1)
    |> narrowDown 2 20
    |> Seq.takeWhile (fun i -> i <= 400000000)
    |> Seq.length

該解決方案在0.018秒內運行。

正如Fyodor Soikin評論的那樣,在seq解決方案中為每次迭代創建一個新數組[|1..20|]是罪魁禍首。 如果我定義一次數組並將其傳入,我可以在10秒內運行它,相比之下,遞歸解決方案需要27秒。 與尾隨調優優化為for循環的遞歸相比,剩余的差異必須降低到延遲序列所需的額外機制。

使isDivisableByAll成為內聯函數isDivisableByAll遞歸解決方案產生重大影響( isDivisableByAll為6秒)。 它似乎不會影響seq解決方案。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM