前段时间我需要使用一种算法来解决 KP 问题,在 Haskell 中
这是我的代码的样子:
stepKP :: [Int] -> (Int, Int) -> [Int]
stepKP l (p, v) = take p l ++ zipWith bestOption l (drop p l)
where bestOption a = max (a+v)
kp :: [(Int, Int)] -> Int -> Int
kp l pMax = last $ foldl stepKP [0 | i <- [0..pMax]] l
main = print $ kp (zip weights values) 20000
where weights = [0..2000]
values = reverse [8000..10000]
但是当我尝试执行它时(用 ghc 编译后,没有标志),它看起来很糟糕:
这是命令 ./kp -RTS -s
1980100
9,461,474,416 bytes allocated in the heap
6,103,730,184 bytes copied during GC
1,190,494,880 bytes maximum residency (18 sample(s))
5,098,848 bytes maximum slop
2624 MiB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 6473 colls, 0 par 2.173s 2.176s 0.0003s 0.0010s
Gen 1 18 colls, 0 par 4.185s 4.188s 0.2327s 1.4993s
INIT time 0.000s ( 0.000s elapsed)
MUT time 3.320s ( 3.322s elapsed)
GC time 6.358s ( 6.365s elapsed)
EXIT time 0.000s ( 0.000s elapsed)
Total time 9.679s ( 9.687s elapsed)
%GC time 0.0% (0.0% elapsed)
Alloc rate 2,849,443,762 bytes per MUT second
Productivity 34.3% of total user, 34.3% of total elapsed
我认为我的程序需要 O(n*w) 内存,而它可以在 O(w) 中完成。 (w为总容量)
这是懒惰评估占用太多空间的问题还是其他问题? 这段代码如何能更有效地节省内存和时间?
答案 0 :(得分:2)
我们可以将左折叠视为执行迭代,同时保留最后返回的累加器。
当有很多迭代时,一个问题是累加器可能会在内存中变得太大。并且因为 Haskell 是惰性的,即使累加器是像 Int
这样的原始类型,也可能发生这种情况:在一些看似无辜的 Int
值后面,可能潜伏着大量未决操作,以 thunk 的形式。
这里严格的左折叠函数 foldl'
很有用,因为它确保在评估左折叠时,累加器将始终保留在 weak head normal form (WHNF) 中。
唉,有时这还不够。 WHNF 只说评估已经进展到值的“最外层构造函数”。这对于 Int
来说已经足够了,但是对于像列表或树这样的递归类型来说,这并没有多大意义:thunk 可能只是潜伏在列表的更下方,或者在下面的分支中。
这里就是这种情况,其中累加器是在每次迭代时重新创建的列表。每次迭代,foldl'
只评估列表到 _ : _
。未评估的 max
和 zipWith
操作开始堆积。
我们需要的是一种在每次迭代时触发对累加器列表的完整评估的方法,该方法可以从内存中清除任何 max
和 zipWith
thunk。而这正是 force
所完成的。当 force $ something
被评估为 WHNF 时,something
被完全评估为 normal form,也就是说,不仅到最外层的构造函数,而且“深入”。
请注意,我们仍然需要 foldl'
以便在每次迭代时“触发”force
。