在Haskell wiki I read that this上:
fib =
let fib' 0 = 0
fib' 1 = 1
fib' n = fib (n - 1) + fib (n - 2)
in (map fib' [0 ..] !!)
比这更有效:
fib x =
let fib' 0 = 0
fib' 1 = 1
fib' n = fib (n - 1) + fib (n - 2)
in map fib' [0 ..] !! x
因为,“在第二种情况下,对于每个参数x都(重新)定义了fib',因此它不能浮出来。”
我不明白这意味着什么。
fib'
每次调用都会重新定义fib
。答案 0 :(得分:24)
这不是一个很好的解释。
“Floated out”仅仅意味着:
\x -> let y = ... in z
如果...
未提及 x ,那么它可以浮出的lambda:
let y = ... in \x -> z
这意味着它只会被计算一次, 1 ,如果...
价格昂贵,可以节省大量时间。但是,GHC对于执行这样的优化是保守的,因为它们会引入空间泄漏。 (虽然如果你给它一个类型签名, 会为第二个定义这样做,正如Daniel Fischer在他的回答中指出的那样。)
但这与自动优化无关。第一个片段在lambda之外定义fib'
,而第二个片段在lambda中定义它(lambda隐含在fib x = ...
中,相当于fib = \x -> ...
),这就是引用的内容
然而,即使这不是真正相关的;相关的是,在第一个片段中,map fib' [0 ..]
发生在lambda之外,因此其结果在lambda的所有应用程序之间共享(在该代码中,“lambda”来自{{1}的部分应用})。在后者中,它位于lambda内部,因此可能会针对(!!)
的每个应用重新计算。
最终结果是前一个实现缓存了值,因此比后者更有效。请注意,第一个代码段的效率取决于fib
不直接递归,而是通过fib'
递归,因此可以从备忘录中受益。
这与eta-expansion有关;后一个片段是第一个的eta扩展。但你引用的陈述并没有解释发生了什么。
1 请注意,这是特定于实现的行为,而不是Haskell语义的一部分。但是,所有合理的实现都将以这种方式运行。
答案 1 :(得分:13)
ehird 的答案解释得非常好,但有一点
最终结果是前一个实现缓存了值,因此远比后者更有效。
这有时是错误的。
如果您编译包含带优化的定义的模块(我只检查了-O2,而不是-O1,当然只检查了GHC),有几种情况需要考虑:
fib :: Int -> Integer
fib :: Num a => Int -> a
fib :: Num a => Int -> a
在案例1中,单态限制产生类型fib :: Int -> Integer
,列表map fib' [0 .. ]
在fib
的所有调用中共享。这意味着如果您查询fib (10^6)
,则会在内存中列出第一个百万(+1)斐波那契数字,并且只有在垃圾收集器可以确定它不再使用时才会收集它。这通常是内存泄漏。
在案例2中,结果(核心)实际上与案例1相同。
在案例4中,列表不是在fib
的不同顶级调用之间共享的(当然;结果可以有很多类型,因此要分享许多列表),但它已实例化每次顶级调用一次并重新用于来自fib'
的调用,因此计算fib n
需要在列表中添加O(n)和O(n ^ 2)步骤。那不算太糟糕。计算完成后收集清单,因此没有空间泄漏。
案例3实际上与4相同。
然而,案例5比天真的递归更糟糕。由于它是显式多态的并且列表绑定在lambda中,因此列表不能重复用于递归调用,每个递归调用都会创建一个新列表...