7

有一个众所周知的解决方案可以生成无限的汉明数流(即所有正整数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
4

3 回答 3

8

Ganesh 是对的,因为您正在多次评估该序列。Seq.cache将有助于提高性能,但您会获得更好的性能,LazyList因为底层序列只评估一次然后缓存,因此可以更快地遍历它。事实上,这是一个很好的例子,说明 whereLazyList 应该使用普通的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 个元素,大约 450 毫秒;前 10000 个元素需要大约 3500 毫秒。

于 2014-07-06T21:10:04.030 回答
3

seq的 forhamming在每次递归调用时从头开始重新评估。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不那么难看的东西。编写一些东西将其转换为 aseq可以更好地处理它:

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恢复seq非常好的语法,尽管我不确定这是否值得。

于 2014-07-06T20:35:43.707 回答
0

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

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
于 2014-07-15T14:53:32.973 回答