在理解和利用GHC自动记忆方面,我碰壁了:用诸如fib 42
之类的固定值调用纯函数时,有时它们又很快又很慢。无论是像fib 42
一样简单地调用它们,还是通过某些数学方法(例如, (\x -> fib (x - 1)) 43
。这些案件似乎没有押韵或理由,因此我将向他们提出这些案件的目的是询问行为背后的逻辑是什么。
考虑一个慢速的斐波那契实现,这使备忘录有效时显而易见:
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
无法记住。
答案 0 :(得分:4)
最后链接的所有示例都使用相同的技术:它们不是直接实现函数f
,而是首先引入一个列表,其内容为对f
的所有调用, 。该列表仅被懒惰地计算一次;然后使用该列表中的简单查找作为面向用户功能的实现。因此,他们不依赖GHC的任何缓存。
您的问题不同:您希望调用某些函数会自动为您缓存,通常不会发生。真正的问题是为什么任何的结果都很快。我不确定,但是我认为这与Constant Applicative Forms(CAF)有关,GHC可以自行决定在多个使用站点之间共享这些{CAF}。
此处CAF最相关的功能是“常量”部分:GHC只会为这样的表达式引入这样的缓存,该表达式的值在程序的整个运行过程中都是恒定的,而不仅仅是在特定范围内。因此,您可以确定f x <> f x
将永远不会重复使用f x
的结果(至少不是由于CAF折叠;也许GHC可以找到其他借口来为某些功能记忆这一点,但通常这样做)不是)。
程序中的两个不是 CAF的事物是slow_fib
的实现和fib_plus_40
的递归情况。 GHC绝对不能对这些表达式的结果进行任何缓存。 fib_plus_40
的基本情况是CAF,main
中的所有表达式和子表达式也是如此。因此,GHC可以选择缓存/共享任何这些子表达式,而不必随意共享它们。也许它认为slow_fib 40
显然很简单,可以保存,但是不确定slow_fib 35
中的main
表达式是否应该共享。同时,听起来确实出于任何原因决定共享IO操作putStrLn $ show $ slow_fib 35
。对您我来说,这似乎是一个奇怪的选择,但我们不是编译器。
这里的道理是,您不能完全依靠它:如果要确保仅计算一次值,则需要将其保存在某个变量中,并引用该变量而不是重新计算。
>为确认这一点,我接受了luqui的建议,并查看了-ddump-simpl
的输出。以下是一些片段,显示了显式缓存:
-- RHS size: {terms: 2, types: 0, coercions: 0}
lvl1_r4ER :: Integer
[GblId, Str=DmdType]
lvl1_r4ER = $wslow_fib_r4EP 40#
Rec {
-- RHS size: {terms: 21, types: 4, coercions: 0}
Main.main_fib_plus_40 [Occ=LoopBreaker] :: Integer -> Integer
[GblId, Arity=1, Str=DmdType <S,U>]
Main.main_fib_plus_40 =
\ (n_a1DF :: Integer) ->
case integer-gmp-1.0.0.1:GHC.Integer.Type.leInteger#
n_a1DF Main.main7
of wild_a2aQ { __DEFAULT ->
case GHC.Prim.tagToEnum# @ Bool wild_a2aQ of _ [Occ=Dead] {
False ->
integer-gmp-1.0.0.1:GHC.Integer.Type.plusInteger
(Main.main_fib_plus_40
(integer-gmp-1.0.0.1:GHC.Integer.Type.minusInteger
n_a1DF Main.main4))
(Main.main_fib_plus_40
(integer-gmp-1.0.0.1:GHC.Integer.Type.minusInteger
n_a1DF lvl_r4EQ));
True -> lvl1_r4ER
}
}
end Rec }
这并不能告诉我们为什么 GHC选择引入此缓存-请记住,它可以做它想做的事情。但是它确实确认了这种机制,它引入了一个变量来保存重复的计算。我无法向您展示包含较小数字的较长main
的核心,因为当我对其进行编译时,会得到更多的共享:第2节中的表达式也为我缓存了。
答案 1 :(得分:4)
详细说明一下,以防@amalloy的答案不清楚,问题在于您将两件事混为一谈-隐式的类似备忘录的行为(人们谈论Haskell的“自动备忘录”时的意思是,虽然这不是真正的备忘录!)直接来自基于基于thunk的惰性评估,以及一种编译器优化技术,该技术基本上是常见子表达式消除的一种形式。前者或多或少是可以预见的。后者是编译器的想法。
请记住, real 记忆是函数实现的属性:该函数“记住”为某些参数组合计算得出的结果,并且可以重新使用这些结果,而不必在调用它们时从头重新计算多次使用相同的参数。当GHC生成功能代码时,它不会自动生成代码来执行这种记录。
相反,生成的GHC代码用于实现功能应用是不寻常的。无需将函数实际应用于参数以生成最终结果作为值,而是立即以thunk的形式构造“结果”,您可以将其视为暂停的函数调用或“承诺”以在以下位置传递值以后。
当在将来的某个时刻需要实际值时,将强制执行thunk(这实际上会导致原始函数调用发生),并使用值更新thunk。如果以后再次需要相同的值,则该值已经可用,因此不需要再次强制更改。这是“自动记忆”。请注意,它发生在“结果”级别而不是“功能”级别-函数应用程序的结果会记住其值;函数不记得它先前产生的结果。
现在,通常,函数应用程序记住其值的结果的概念将是荒谬的。在严格的语言中,我们不用担心在x = sqrt(10)
之后重复使用x
会导致多次sqrt
调用,因为x
尚未“记忆”其自身的价值。也就是说,在严格的语言中,所有功能应用程序的结果都将按照在Haskell中的含义进行“自动记忆”。
区别在于懒惰的评估,这使我们可以编写如下内容:
stuff = map expensiveComputation [1..10000]
会立即返回一个thunk,而无需执行任何昂贵的计算。然后:
f n = stuff !! n
神奇地创建了一个记忆函数,不是因为GHC在f
的实现中生成了代码以某种方式来记忆调用f 1000
,而是因为f 1000
强制了(一堆列表构造函数的重击和然后)一个expensiveComputation
,其返回值被“存储”为列表stuff
中的索引1000的值–是一个重击,但是在被强制之后,它会记住自己的值,就像用严格的语言来表达任何价值。
因此,根据您对slow_fib
的定义,您的所有示例实际上都没有使用Haskell的自动备忘功能,这在人们的通常意义上是正常的。您看到的任何加速都是各种编译器优化的结果,这些优化是(或没有)识别常见的子表达式或内联/展开短循环。
要编写记忆的fib
,您需要像使用严格的语言一样显式地执行此操作,方法是创建一个数据结构来保存该记忆的值,尽管惰性求值和相互递归的定义有时会使它似乎是“自动”的:
import qualified Data.Vector as V
import Data.Vector (Vector,(!))
fibv :: Vector Integer
fibv = V.generate 1000000 getfib
where getfib 0 = 1
getfib 1 = 1
getfib i = fibv ! (i-1) + fibv ! (i-2)
fib :: Int -> Integer
fib n = fibv ! n