簡體   English   中英

seq的性能 <int> vs Lazy <LazyList<int> &gt;在F#

[英]Performance of seq<int> vs Lazy<LazyList<int>> in F#

存在用於生成無限漢明數的流的公知解決方案(即,所有正整數n ,其中n = 2^i * 3^j * 5^k )。 我在F#中以兩種不同的方式實現了這一點。 第一種方法使用seq<int> 解決方案很優雅,但性能很糟糕。 第二種方法使用自定義類型,其中尾部包裝在Lazy<LazyList<int>> 解決方案很笨重,但性能卻令人驚嘆。

有人可以解釋為什么使用seq<int>的性能是如此糟糕,如果有辦法解決它? 謝謝。

方法1使用seq<int>

// 2-way merge with deduplication
let rec (-|-) (xs: seq<int>) (ys: seq<int>) =
    let x = Seq.head xs
    let y = Seq.head ys
    let xstl = Seq.skip 1 xs
    let ystl = Seq.skip 1 ys
    if x < y then seq { yield x; yield! xstl -|- ys }
    elif x > y then seq { yield y; yield! xs -|- ystl }
    else seq { yield x; yield! xstl -|- ystl }

let rec hamming: seq<int> = seq {
    yield 1
    let xs = Seq.map ((*) 2) hamming
    let ys = Seq.map ((*) 3) hamming
    let zs = Seq.map ((*) 5) hamming
    yield! xs -|- ys -|- zs
}

[<EntryPoint>]
let main argv = 
    Seq.iter (printf "%d, ") <| Seq.take 100 hamming
    0

方法2使用Lazy<LazyList<int>>

type LazyList<'a> = Cons of 'a * Lazy<LazyList<'a>>

// Map `f` over an infinite lazy list
let rec inf_map f (Cons(x, g)) = Cons(f x, lazy(inf_map f (g.Force())))

// 2-way merge with deduplication
let rec (-|-) (Cons(x, f) as xs) (Cons(y, g) as ys) =
    if x < y then Cons(x, lazy(f.Force() -|- ys))
    elif x > y then Cons(y, lazy(xs -|- g.Force()))
    else Cons(x, lazy(f.Force() -|- g.Force()))

let rec hamming =
    Cons(1, lazy(let xs = inf_map ((*) 2) hamming
                 let ys = inf_map ((*) 3) hamming
                 let zs = inf_map ((*) 5) hamming
                 xs -|- ys -|- zs))

[<EntryPoint>]
let main args =
    let a = ref hamming
    let i = ref 0
    while !i < 100 do
        match !a with
        | Cons (x, f) ->
            printf "%d, " x
            a := f.Force()
            i := !i + 1
    0

Ganesh是正確的,因為你正在多次評估序列。 Seq.cache將有助於提高性能,但是您可以從LazyList獲得更好的性能,因為基礎序列只會被評估一次然后被緩存,因此它可以更快地遍歷。 實際上,這是一個很好的例子,說明LazyList 應該在普通seq

看起來你在這里使用Seq.map引入了一些重大的開銷。 我相信編譯器每次調用時都會分配一個閉包。 我將基於seq的代碼更改為使用seq -expressions代替,並且它比序列中前40個數字的原始代碼快1/3:

let rec hamming: seq<int> = seq {
    yield 1
    let xs = seq { for x in hamming do yield x * 2 }
    let ys = seq { for x in hamming do yield x * 3 }
    let zs = seq { for x in hamming do yield x * 5 }
    yield! xs -|- ys -|- zs
}

我的ExtCore庫包含一個lazyList計算構建器,它就像seq一樣工作,因此您可以像這樣簡化代碼:

// 2-way merge with deduplication
let rec (-|-) (xs: LazyList<'T>) (ys: LazyList<'T>) =
    let x = LazyList.head xs
    let y = LazyList.head ys
    let xstl = LazyList.skip 1 xs
    let ystl = LazyList.skip 1 ys
    if x < y then lazyList { yield x; yield! xstl -|- ys }
    elif x > y then lazyList { yield y; yield! xs -|- ystl }
    else lazyList { yield x; yield! xstl -|- ystl }

let rec hamming : LazyList<uint64> = lazyList {
    yield 1UL
    let xs = LazyList.map ((*) 2UL) hamming
    let ys = LazyList.map ((*) 3UL) hamming
    let zs = LazyList.map ((*) 5UL) hamming
    yield! xs -|- ys -|- zs
}

[<EntryPoint>]
let main argv =
    let watch = Stopwatch.StartNew ()

    hamming
    |> LazyList.take 2000
    |> LazyList.iter (printf "%d, ")

    watch.Stop ()
    printfn ""
    printfn "Elapsed time: %.4fms" watch.Elapsed.TotalMilliseconds

    System.Console.ReadKey () |> ignore
    0   // Return an integer exit code

(注意:我還使你的(-|-)函數通用,並修改hamming使用64位無符號整數,因為32位有符號整數后溢出。) 這段代碼在我的機器上運行序列的前2000個元素~45ms; 前10000個元素需要~3500ms。

在每次遞歸調用時,都會從頭開始重新評估hamming seq Seq.cache是一些幫助:

let rec hamming: seq<int> =
    seq {
        yield 1
        let xs = Seq.map ((*) 2) hamming
        let ys = Seq.map ((*) 3) hamming
        let zs = Seq.map ((*) 5) hamming
        yield! xs -|- ys -|- zs
    } |> Seq.cache

但是,正如您指出的那樣,即使每個序列都被緩存, LazyList在大輸入上仍然要好得多。

我不完全確定為什么它們的區別不僅僅是一個小的常數因子,但也許最好只關注使LazyList不那么難看。 寫一些東西將其轉換為seq會使處理得更好:

module LazyList =
    let rec toSeq l =
        match l with
        | Cons (x, xs) ->
            seq {
                yield x
                yield! toSeq xs.Value
            }

然后,您可以直接使用簡單的main 使用變異來處理LazyList也沒有必要,你可以遞歸地這樣做。

雖然lazyForce()會使它混亂,但定義看起來並不那么糟糕。 如果使用.Value而不是.Force()那看起來會略微好一點。 您還可以為LazyList定義一個類似於seq計算構建器來恢復非常好的語法,盡管我不確定這是值得的。

這是一個具有更好性能的序列庫版本。

let hamming =
    let rec loop nextHs =
        seq {
            let h = nextHs |> Set.minElement
            yield h
            yield! nextHs 
                |> Set.remove h 
                |> Set.add (h*2) |> Set.add (h*3) |> Set.add (h*5) 
                |> loop
            }

    Set.empty<int> |> Set.add 1 |> loop

暫無
暫無

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

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