【发布时间】:2019-11-21 20:51:17
【问题描述】:
在我理解和利用 GHC 自动记忆的过程中,我碰壁了:当纯函数使用固定值(如 fib 42)调用时,再次调用它们时有时快有时慢。如果它们被简单地称为fib 42 或通过一些数学隐含地调用它们,它会有所不同,例如(\x -> fib (x - 1)) 43。这些案例似乎没有押韵或理由,所以我将呈现它们的目的是询问行为背后的逻辑是什么。
考虑一个缓慢的 Fibonacci 实现,这使得 memoization 工作时很明显:
slow_fib :: Int -> Integer
slow_fib n = if n < 2 then 1 else (slow_fib (n - 1)) + (slow_fib (n - 2))
我测试了三个基本问题,看看 GHC(8.2.2 版)是否会记忆带有固定参数的调用:
-
slow_fib可以访问以前对slow_fib的顶级调用吗? - 是否已为以后的重要(例如数学)顶级表达式存储先前的结果?
- 是否为以后相同的顶级表达式记住了以前的结果?
答案似乎是:
- 没有
- 没有
- 是的 [??]
最后一个案例有效的事实让我非常困惑:例如,如果我可以重新打印结果,那么我应该期望能够添加它们。这是显示这一点的代码:
main = do
-- 1. all three of these are slow, even though `slow_fib 37` is
-- just the sum of the other two results. Definitely no memoization.
putStrLn $ show $ slow_fib 35
putStrLn $ show $ slow_fib 36
putStrLn $ show $ slow_fib 37
-- 2. also slow, definitely no memoization as well.
putStrLn $ show $ (slow_fib 35) + (slow_fib 36) + (slow_fib 37)
putStrLn $ show $ (slow_fib 35) + 1
-- 3. all three of these are instant. Huh?
putStrLn $ show $ slow_fib 35
putStrLn $ show $ slow_fib 36
putStrLn $ show $ slow_fib 37
然而奇怪的是,当结果嵌入到递归函数中时,对结果进行数学运算:这个从 Fib(40) 开始的斐波那契变体:
let fib_plus_40 n = if n <= 0
then slow_fib 40
else (fib_plus_40 (n - 1)) + (fib_plus_40 (n - 2))
如下所示:
main = do
-- slow as expected
putStrLn $ show $ fib_plus_40 0
-- instant. Why?!
putStrLn $ show $ fib_plus_40 1
我在 GHC 记忆化的任何解释中找不到任何理由,这通常会导致显式变量(例如 here、here、and here)。这就是为什么我预计 fib_plus_40 无法记忆的原因。
【问题讨论】:
-
好问题。不过,时间安排很棘手——你应该学会阅读这些实验的核心。如果我记得的话,用
-ddump-simpl编译 -
我希望在您的第一个示例中,实际上不是编译器已提取
slow_fib 35作为表达式在对putStrLn的两次调用之间共享;而是putStrLn $ show $ slow_fib 35它自己 已被拉出来分享。这解释了为什么putStrLn $ show $ slow_fib35 + 1仍然很慢:它是一个单独的(可能是记忆的!)动作,仍然必须进行计算。实际上,您可以验证,如果您输入第二个putStrLn $ show $ slow_fib35 + 1,它也会立即执行,因此这不是您建议的“微不足道与非微不足道”的区别。 -
@DanielWagner 啊,是的,这同样令人惊讶。看起来它只是完全不可预测的。
标签: haskell ghc memoization