什么“浮出来”是什么意思?

时间:2012-01-08 17:01:11

标签: haskell

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',因此它不能浮出来。”

我不明白这意味着什么。

  1. “飘出来”是什么意思?它是如何优化的?
  2. 为什么fib'每次调用都会重新定义fib
  3. 这是否是eta-expansion?

2 个答案:

答案 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),有几种情况需要考虑:

  1. 没有类型签名的第一个定义
  2. 带有类型签名fib :: Int -> Integer
  3. 的第二个定义
  4. 多态类型fib :: Num a => Int -> a
  5. 的第一个定义
  6. 没有类型签名的第二个定义
  7. 带有类型签名fib :: Num a => Int -> a
  8. 的第二个定义

    在案例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中,因此列表不能重复用于递归调用,每个递归调用都会创建一个新列表...