阅读memoization introduction之后,我通过使用更一般的memoize函数重新实现了Fibonacci示例(仅用于学习目的):
memoizer :: (Int -> Integer) -> Int -> Integer
memoizer f = (map f [0 ..] !!)
memoized_fib :: Int -> Integer
memoized_fib = memoizer fib
where fib 0 = 0
fib 1 = 1
fib n = memoized_fib (n-2) + memoized_fib (n-1)
这样可行,但是当我将最后一行更改为以下代码时,memoization突然无法正常工作(程序再次变慢):
fib n = memoizer fib (n-2) + memoizer fib (n-1)
关键区别w.r.t.记忆?
答案 0 :(得分:6)
它是关于显式与隐式共享。当你明确地命名一个东西时,它自然可以被共享,即在内存中作为单独的实体存在,并被重用。 (当然共享不是语言本身的一部分,我们只能轻微地推动编译器分享某些东西)。
但是当您将相同的表达式写入两次或三次时,您依赖于编译器将公共子表达式替换为一个显式共享实体。这可能会也可能不会发生。
您的第一个变体等同于
memoized_fib :: Int -> Integer
memoized_fib = (map fib [0 ..] !!) where
fib 0 = 0
fib 1 = 1
fib n = memoized_fib (n-2) + memoized_fib (n-1)
在这里,您专门命名一个实体,并通过该名称引用它。但这是一个功能。为了使重用更加确定,我们可以明确地命名在此处共享的实际值列表:
memoized_fib :: Int -> Integer
memoized_fib = (fibs !!) where
fibs = map fib [0 ..]
fib 0 = 0
fib 1 = 1
fib n = memoized_fib (n-2) + memoized_fib (n-1)
使用显式引用此处共享的实际实体 - 最后一行可以更直观地显示 - 我们在上面的步骤中命名的列表fibs
: / p>
fib n = fibs !! (n-2) + fibs !! (n-1)
您的第二个变体与此相同:
memoized_fib :: Int -> Integer
memoized_fib = (map fib [0 ..] !!) where
fib 0 = 0
fib 1 = 1
fib n = (map fib [0 ..] !!) (n-2) + (map fib [0 ..] !!) (n-1)
这里我们有三个看似独立的map
表达式,它们可能会也可能不会被编译器共享。 使用ghc -O2
进行编译似乎重新引入了共享,并且速度很快。
答案 1 :(得分:3)
momoized_fib = ...
- 这是顶级的简单定义。它可能被读作一个常量的惰性值(在扩展它之前没有任何额外的参数需要绑定。这是你的memoized值的“源”。
使用(memoizer fib) (n-2)
时,会创建与memoized_fib
无关的新值来源,因此不会重复使用。实际上你在GC上移动了很多负载,因为你在第二个变种中产生了很多(map fib [0 ..])
个序列。
还可以考虑更简单的例子:
f = \n -> sq !! n where sq = [x*x | x <- [0 ..]]
g n = sq !! n where sq = [x*x | x <- [0 ..]]
首先会生成单个f
并与之关联sq
,因为声明头中没有n
。第二个将为f n
的每个不同值生成一系列列表并移过它(不限于实际值)以获得值。