【问题标题】:Performance of seq<int> vs Lazy<LazyList<int>> in F#F# 中 seq<int> 与 Lazy<LazyList<int>> 的性能
【发布时间】:2014-08-27 07:32:45
【问题描述】:

有一个众所周知的解决方案可以生成无限的汉明数流(即所有正整数n,其中n = 2^i * 3^j * 5^k)。我在 F# 中以两种不同的方式实现了这一点。第一种方法使用seq&lt;int&gt;。解决方案很优雅,但性能很糟糕。第二种方法使用自定义类型,其中尾部包裹在Lazy&lt;LazyList&lt;int&gt;&gt; 中。解决方案笨拙,但性能惊人。

有人可以解释为什么使用seq&lt;int&gt; 的性能如此糟糕,是否有办法解决它?谢谢。

方法一使用seq&lt;int&gt;。

// 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

使用Lazy&lt;LazyList&lt;int&gt;&gt;的方法2。

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

【问题讨论】:

    标签: f# infinite seq hamming-numbers


    【解决方案1】:

    Ganesh 是正确的,因为您正在多次评估序列。 Seq.cache 将有助于提高性能,但您可以从 LazyList 获得更好的性能,因为底层序列只评估一次然后缓存,因此可以更快地遍历它。事实上,这是一个很好的例子,说明LazyList 应该 用于普通的seq。

    您在此处使用Seq.map 似乎也引入了一些重大开销。我相信编译器在每次调用时都会分配一个闭包。我将您的基于seq 的代码更改为在那里使用seq-表达式,对于序列中的前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 个元素,大约 450 毫秒;前 10000 个元素大约需要 3500 毫秒。

    【讨论】:

    • 我很惊讶seq-comprehension 比Seq.map 快得多。我也试图让它与 F# PowerPack 一起工作,但到目前为止我还没有成功。
    • @fysx 使用我的 ExtCore 库 - 它包含来自 F# PowerPack 的 LazyList 代码的更新(和维护)版本。它还有一个lazyList 构建器,大大简化了这种代码的编写。
    • @fysx seq-comprehension 通常更快,因为它可以被编译器更好地优化。 Seq.map 本身并不慢,但在这种情况下会有很多开销,因为代码会重复评估序列,并且每次调用 Seq.map 时编译的代码都必须分配一个闭包。
    • 我使用了包管理器控制台和“Install-Package ExtCore”。没有main,但只有(-|-) 和hamming,我得到“这个表达式应该有LazyList&lt;'a&gt; 类型,但这里有LazyList&lt;uint64&gt; 类型hamming。我正在使用 Visual Studio 2013。我尝试指定类型,但通常lazyList 和hamming 之间存在一些类型不匹配。伤心。
    • @fysx 这很奇怪——我编译上面的代码没有问题,即使我注释掉了main。你介意在 ExtCore github 页面上打开一个关于这个的问题吗?我想帮你解决问题,把讨论移到那里很容易。
    【解决方案2】:

    您的seq 用于hamming 会在每次递归调用时从头开始重新评估。 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,你可以递归地这样做。

    虽然lazy 和Force() 确实有点混乱,但定义看起来并不那么糟糕。如果您使用.Value 而不是.Force(),那看起来会稍微好一些。您还可以为LazyList 定义一个computation builder,类似于seq 来恢复非常好的语法,尽管我不确定这是否值得。

    【讨论】:

    • 在我使用seq {...} 后,在整个过程中喷洒|&gt; Seq.cache 确实提高了性能,但并没有显着提高(特别是如果我在序列中计算更大的数字。我注意到stackoverflow.com/questions/9201402/sequence-vs-lazylist,这似乎是对你的建议的补充好吧。
    • 对于 1..100,与非常慢相比,它几乎是瞬时的。您的惰性列表仍然大大优于它的典型数字是多少?
    • 好吧,我发现它对于 1000 并不能很好地工作,即使我在每个序列上都加上 Seq.cache,10000 也是一个巨大的损失。
    • 试试 1500。LazyList 版本应该明显更快。
    • ExtCore 包含一个 lazyList 计算构建器,因此您无需构建自己的 :)
    【解决方案3】:

    这是一个性能更好的序列基础版本。

    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
    

    【讨论】:

      猜你喜欢
      • 2020-08-12
      • 1970-01-01
      • 2011-02-22
      • 1970-01-01
      • 2013-01-26
      • 2013-12-31
      • 2012-12-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多