【问题标题】:Solving knapsack prob in F#: performance在 F# 中解决背包问题:性能
【发布时间】:2013-07-01 00:24:28
【问题描述】:

我找到了一篇文章:
Solving the 0-1 knapsack problem using continuation-passing style with memoization in F#

关于在 F# 中实现的背包问题。当我学习这门语言时,我发现这真的很有趣,并试图对此进行一些调查。这是我编写的代码:

open System
open System.IO 
open System.Collections.Generic

let parseToTuple (line : string) =
    let parsedLine = line.Split(' ') |> Array.filter(not << String.IsNullOrWhiteSpace)         |> Array.map Int32.Parse
    (parsedLine.[0], parsedLine.[1])

let memoize f =
    let cache = Dictionary<_, _>()
    fun x ->
        if cache.ContainsKey(x)
            then cache.[x]
        else
            let res = f x
            cache.[x] <- res
            res

type Item =
    {
        Value : int
        Size  : int
    }  

type ContinuationBuilder() = 
    member b.Bind(x, f) = fun k -> x (fun x -> f x k)
    member b.Return x = fun k ->  k x
    member b.ReturnFrom x = x

let cont = ContinuationBuilder()

let set1 =
    [
        (4, 11)
        (8, 4)
        (10, 5)
        (15, 8)
        (4, 3)
    ]

let set2 =
    [
        (50, 341045); (1906, 4912); (41516, 99732); (23527, 56554); (559, 1818); (45136, 108372); (2625, 6750); (492, 1484)
        (1086, 3072); (5516, 13532); (4875, 12050); (7570, 18440); (4436, 10972); (620, 1940); (50897, 122094); (2129, 5558)
        (4265, 10630); (706, 2112); (2721, 6942); (16494, 39888); (29688, 71276); (3383, 8466); (2181, 5662); (96601, 231302)
        (1795, 4690); (7512, 18324); (1242, 3384); (2889, 7278); (2133, 5566); (103, 706); (4446, 10992); (11326, 27552)
        (3024, 7548); (217, 934); (13269, 32038); (281, 1062); (77174, 184848); (952, 2604); (15572, 37644); (566, 1832)
        (4103, 10306); (313, 1126); (14393, 34886); (1313, 3526); (348, 1196); (419, 1338); (246, 992); (445, 1390)
        (23552, 56804); (23552, 56804); (67, 634)
    ]

[<EntryPoint>] 
let main args =
    // prepare list of items from a file args.[0]
    let header, items = set1
                        |> function
                           | h::t -> h, t
                           | _    -> raise (Exception("Wrong data format"))

    let N, K = header
    printfn "N = %d, K = %d" N K
    let items = List.map (fun x -> {Value = fst x ; Size = snd x}) items |> Array.ofList

    let rec combinations =
        let innerSolver key =
            cont
                {
                    match key with
                    | (i, k) when i = 0 || k = 0        -> return 0
                    | (i, k) when items.[i-1].Size > k  -> return! combinations (i-1, k)
                    | (i, k)                            -> let item = items.[i-1]
                                                           let! v1 = combinations (i-1, k)
                                                           let! beforeItem = combinations (i-1, k-item.Size)
                                                           let v2 = beforeItem + item.Value
                                                           return max v1 v2
                }
        memoize innerSolver

    let res = combinations (N, K) id
    printfn "%d" res
    0

然而,这个实现的问题是它非常缓慢(实际上我无法解决 50 个项目和约 300000 容量的问题,我在 C# 中的幼稚实现在不到 1 秒内就解决了这个问题)。

如果我在某个地方犯了错误,你能告诉我吗?或者也许实现是正确的,而这只是解决这个问题的低效方法。

【问题讨论】:

  • 标准 F# 性能 cmets:可能避免继续。避免使用列表,使用数组。尝试逐行翻译 C# 并进行比较。此外,请注意可能很慢的比较运算符并检查您的编译器选项。
  • 考虑到您的测试的最小尺寸,我猜测您的代码中某处存在逻辑错误。您是否使用 5 个项目验证了您的代码?
  • 你分析了吗?
  • @ ildjarn 1.) 我仔细检查了 - 我正在测试发布版本。此外,我了解到 Debug 默认不支持尾递归,所以如果我在更大的实例上运行它,我会得到 StackOverflow 异常。 2.) 你到底不喜欢什么?我按照文章中的说明进行操作,但是我是第一次这样做,所以可能会有错误。 @mydogisbox 那是因为递归隐藏在计算表达式中 - 注意让!语句和 Bind 方法。
  • 您是否可以从您的问题中删除输入解析,而是提供一些具体的输入,以便想要对其进行分析并向您提供有关性能反馈的人可以立即运行您的代码? (谢谢!)

标签: performance optimization f# knapsack-problem


【解决方案1】:

当您像这样天真地应用通用记忆器并使用延续传递时,记忆缓存中的值是延续,而不是常规的“最终”结果。因此,当您获得缓存命中时,您不会返回最终结果,而是返回一些承诺在您调用它时计算结果的函数。此调用可能很昂贵,可能会调用各种其他延续,最终可能会再次访问 memoization 缓存,等等。

有效地记忆连续传递函数,以便a) 缓存发挥最大作用和b) 函数保持尾递归是相当困难的。阅读this 讨论并在您完全理解后回来。 ;-)

您链接的博客文章的作者正在使用一种更复杂、更通用的记忆器,该记忆器专门适用于该问题。诚然,我还没有完全理解它(博客上的代码不完整/损坏,所以很难尝试),但我认为它的要点是它在缓存最终整数之前“强制”延续链结果。

为了说明这一点,这里是您的代码的快速重构,它是完全独立的并跟踪相关信息:

open System
open System.Collections.Generic

let mutable cacheHits = 0
let mutable cacheMisses = 0

let memoize f =
    let cache = Dictionary<_, _>()
    fun x ->
        match cache.TryGetValue(x) with
        | (true, v) -> 
            cacheHits <- cacheHits + 1
            printfn "Hit for %A - Result is %A" x v
            v
        | _ ->
            cacheMisses <- cacheMisses + 1
            printfn "Miss for %A" x
            let res = f x
            cache.[x] <- res
            res

type Item = { Value : int; Size  : int }  

type ContinuationBuilder() = 
    member b.Bind(x, f) = fun k -> x (fun x -> f x k)
    member b.Return x = fun k ->  k x
    member b.ReturnFrom x = x

let cont = ContinuationBuilder()

let genItems n = 
   [| for i = 1 to n do
         let size = i % 5
         let value = (size * i)
         yield { Value = value; Size = size }
   |]

let N, K = (5, 100)
printfn "N = %d, K = %d" N K

let items = genItems N

let rec combinations_cont =
    memoize (
     fun key ->
       cont {
                match key with
                | (0, _) | (_, 0)                   -> return 0
                | (i, k) when items.[i-1].Size > k  -> return! combinations_cont (i - 1, k) 
                | (i, k)                            -> let item = items.[i-1]
                                                       let! v1 = combinations_cont (i-1, k)
                                                       let! beforeItem = combinations_cont (i-1, k - item.Size)
                                                       let v2 = beforeItem + item.Value
                                                       return max v1 v2
        }
    )

let res = combinations_cont (N, K) id
printfn "Answer: %d" res
printfn "Memo hits: %d" cacheHits
printfn "Memo misses: %d" cacheMisses
printfn ""

let rec combinations_plain =
    memoize (
     fun key ->
                match key with
                | (i, k) when i = 0 || k = 0        -> 0
                | (i, k) when items.[i-1].Size > k  -> combinations_plain (i-1, k) 
                | (i, k)                            -> let item = items.[i-1]
                                                       let v1 = combinations_plain (i-1, k)
                                                       let beforeItem = combinations_plain (i-1, k-item.Size)
                                                       let v2 = beforeItem + item.Value
                                                       max v1 v2
    )

cacheHits <- 0
cacheMisses <- 0

let res2 = combinations_plain (N, K)
printfn "Answer: %d" res2
printfn "Memo hits: %d" cacheHits
printfn "Memo misses: %d" cacheMisses

如您所见,CPS 版本正在缓存延续(不是整数),并且在调用延续时会有很多额外的活动在最后进行。

如果您将问题大小增加到 let (N, K) = (20, 100)(并删除记忆器中的 printfn 语句),您将看到 CPS 版本最终进行了超过 100 万次缓存查找,而普通版本只进行了几次百。

【讨论】:

    【解决方案2】:

    在 FSI 中运行此代码:

    open System
    open System.Diagnostics
    open System.Collections.Generic
    
    let time f =
        System.GC.Collect()
        let sw = Stopwatch.StartNew()
        let r = f()
        sw.Stop()
        printfn "Took: %f" sw.Elapsed.TotalMilliseconds
        r
    
    let mutable cacheHits = 0
    let mutable cacheMisses = 0
    
    let memoize f =
        let cache = Dictionary<_, _>()
        fun x ->
            match cache.TryGetValue(x) with
            | (true, v) -> 
                cacheHits <- cacheHits + 1
                //printfn "Hit for %A - Result is %A" x v
                v
            | _ ->
                cacheMisses <- cacheMisses + 1
                //printfn "Miss for %A" x
                let res = f x
                cache.[x] <- res
                res
    
    type Item = { Value : int; Size  : int }  
    
    type ContinuationBuilder() = 
        member b.Bind(x, f) = fun k -> x (fun x -> f x k)
        member b.Return x = fun k ->  k x
        member b.ReturnFrom x = x
    
    let cont = ContinuationBuilder()
    
    let genItems n = 
        [| for i = 1 to n do
                let size = i % 5
                let value = (size * i)
                yield { Value = value; Size = size }
        |]
    
    let N, K = (80, 400)
    printfn "N = %d, K = %d" N K
    
    let items = genItems N
    
    //let rec combinations_cont =
    //    memoize (
    //     fun key ->
    //       cont {
    //                match key with
    //                | (0, _) | (_, 0)                   -> return 0
    //                | (i, k) when items.[i-1].Size > k  -> return! combinations_cont (i - 1, k) 
    //                | (i, k)                            -> let item = items.[i-1]
    //                                                       let! v1 = combinations_cont (i-1, k)
    //                                                       let! beforeItem = combinations_cont (i-1, k - item.Size)
    //                                                       let v2 = beforeItem + item.Value
    //                                                       return max v1 v2
    //        }
    //    )
    //
    //
    //cacheHits <- 0
    //cacheMisses <- 0
    
    //let res = time(fun () -> combinations_cont (N, K) id)
    //printfn "Answer: %d" res
    //printfn "Memo hits: %d" cacheHits
    //printfn "Memo misses: %d" cacheMisses
    //printfn ""
    
    let rec combinations_plain =
        memoize (
            fun key ->
                    match key with
                    | (i, k) when i = 0 || k = 0        -> 0
                    | (i, k) when items.[i-1].Size > k  -> combinations_plain (i-1, k) 
                    | (i, k)                            -> let item = items.[i-1]
                                                           let v1 = combinations_plain (i-1, k)
                                                           let beforeItem = combinations_plain (i-1, k-item.Size)
                                                           let v2 = beforeItem + item.Value
                                                           max v1 v2
        )
    
    cacheHits <- 0
    cacheMisses <- 0
    
    printfn "combinations_plain"
    let res2 = time (fun () -> combinations_plain (N, K))
    printfn "Answer: %d" res2
    printfn "Memo hits: %d" cacheHits
    printfn "Memo misses: %d" cacheMisses
    printfn ""
    
    let recursivelyMemoize f =
        let cache = Dictionary<_, _>()
        let rec memoizeAux x =
            match cache.TryGetValue(x) with
            | (true, v) -> 
                cacheHits <- cacheHits + 1
                //printfn "Hit for %A - Result is %A" x v
                v
            | _ ->
                cacheMisses <- cacheMisses + 1
                //printfn "Miss for %A" x
                let res = f memoizeAux x
                cache.[x] <- res
                res
        memoizeAux
    
    let combinations_plain2 =
        let combinations_plain2Aux combinations_plain2Aux key =
                    match key with
                    | (i, k) when i = 0 || k = 0        -> 0
                    | (i, k) when items.[i-1].Size > k  -> combinations_plain2Aux (i-1, k) 
                    | (i, k)                            -> let item = items.[i-1]
                                                           let v1 = combinations_plain2Aux (i-1, k)
                                                           let beforeItem = combinations_plain2Aux (i-1, k-item.Size)
                                                           let v2 = beforeItem + item.Value
                                                           max v1 v2
        let memoized = recursivelyMemoize combinations_plain2Aux
        fun x -> memoized x
    
    cacheHits <- 0
    cacheMisses <- 0
    
    printfn "combinations_plain2"
    let res3 = time (fun () -> combinations_plain2 (N, K))
    printfn "Answer: %d" res3
    printfn "Memo hits: %d" cacheHits
    printfn "Memo misses: %d" cacheMisses
    printfn ""
    
    let recursivelyMemoizeCont f =
        let cache = Dictionary HashIdentity.Structural
        let rec memoizeAux x k =
            match cache.TryGetValue(x) with
            | (true, v) -> 
                cacheHits <- cacheHits + 1
                //printfn "Hit for %A - Result is %A" x v
                k v
            | _ ->
                cacheMisses <- cacheMisses + 1
                //printfn "Miss for %A" x
                f memoizeAux x (fun y ->
                    cache.[x] <- y
                    k y)
        memoizeAux
    
    let combinations_cont2 =
        let combinations_cont2Aux combinations_cont2Aux key =
            cont {
                    match key with
                    | (0, _) | (_, 0)                   -> return 0
                    | (i, k) when items.[i-1].Size > k  -> return! combinations_cont2Aux (i - 1, k) 
                    | (i, k)                            -> let item = items.[i-1]
                                                           let! v1 = combinations_cont2Aux (i-1, k)
                                                           let! beforeItem = combinations_cont2Aux (i-1, k - item.Size)
                                                           let v2 = beforeItem + item.Value
                                                           return max v1 v2
            }
        let memoized = recursivelyMemoizeCont combinations_cont2Aux
        fun x -> memoized x id
    
    cacheHits <- 0
    cacheMisses <- 0
    
    printfn "combinations_cont2"
    let res4 = time (fun () -> combinations_cont2 (N, K))
    printfn "Answer: %d" res4
    printfn "Memo hits: %d" cacheHits
    printfn "Memo misses: %d" cacheMisses
    printfn ""
    

    我得到了这些结果:

    N = 80, K = 400
    combinations_plain
    Took: 7.191000
    Answer: 6480
    Memo hits: 6231
    Memo misses: 6552
    
    combinations_plain2
    Took: 6.310800
    Answer: 6480
    Memo hits: 6231
    Memo misses: 6552
    
    combinations_cont2
    Took: 17.021200
    Answer: 6480
    Memo hits: 6231
    Memo misses: 6552
    
    • combinations_plain 来自 latkin 的回答。
    • combinations_plain2 显式公开了递归记忆步骤。
    • combinations_cont2 将递归记忆功能改编为记忆延续结果的功能。
    • combinations_cont2 通过在将结果传递给实际延续之前拦截延续中的结果来工作。对同一键的后续调用提供了一个延续,并且这个延续是我们最初截获的答案。

    这表明我们能够:

    1. 使用延续传递样式进行记忆。
    2. 实现与普通记忆版本相似 (ish) 的性能特征。

    我希望这能让事情变得更清楚。抱歉,我的博客代码 sn-p 不完整(我想我最近重新格式化时可能丢失了它)。

    【讨论】:

      猜你喜欢
      • 2012-06-17
      • 1970-01-01
      • 2021-03-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-08-11
      相关资源
      最近更新 更多