【问题标题】:Why is this version of 'fix' more efficient in Haskell?为什么这个版本的“修复”在 Haskell 中更有效?
【发布时间】:2016-09-18 20:55:40
【问题描述】:

在 Haskell 中,这是一个简单(朴素)的不动点定义

fix :: (a -> a) -> a
fix f = f (fix f)

但是,这是 Haskell 实际实现它的方式(更高效)

fix f = let x = f x in x

我的问题是为什么第二个比第一个更有效?

【问题讨论】:

标签: haskell fixpoint-combinators


【解决方案1】:

慢的fix 在递归的每一步调用f,而快的则只调用一次f。可以通过追踪来可视化:

import Debug.Trace

fix  f = f (fix f)
fix' f = let x = f x in x

facf :: (Int -> Int) -> Int -> Int
facf f 0 = 1
facf f n = n * f (n - 1)

tracedFacf x = trace "called" facf x

fac  = fix tracedFacf
fac' = fix' tracedFacf

现在尝试一些运行:

> fac 3
called
called
called
called
6
> fac' 3
called
6

更详细地说,let x = f x in x 导致为 x 分配一个惰性 thunk,并将指向此 thunk 的指针传递给 f。在第一次评估 fix' f 时,thunk 被评估并且所有对它的引用(这里特别是:我们传递给 f 的引用)都被重定向到结果值。碰巧x 被赋予了一个值,该值还包含对x 的引用。

我承认这可能相当令人费解。与懒惰一起工作时应该习惯这一点。

【讨论】:

  • 我想我今天有点无法解析英语,但如果你说“只调用 f 一次”,你不是在谈论评估 f 对吗?因为很明显,无论涉及什么魔法,您都必须调用facf 几次(将trace 移动到facf 会显示它)
  • facf 不是递归的,我们调用它一次,fix' 返回一个函数对象。当我们使用数字参数调用 that 函数对象时,它可能会多次递归调用自身(如您所说,可以再次跟踪)。
  • 感谢您的解释。我有一个更一般的问题。可以通过使用这种let 绑定方法来“优化”所有递归函数吗?如果是这样,为什么 GHC 内部不使用这种技术来优化递归?我认为编写递归函数的朴素方式更容易阅读和理解。
  • @VijayaRani 简单的递归定义已经尽可能快且可优化。与简单的递归定义相比,只是缓慢的 fix 定义引入了开销。
  • 这实际上是由语言规范保证的,还是依赖于一个特定实现的一个特定版本的私有内部实现细节?
【解决方案2】:

当您使用带有两个参数的函数调用fix 来生成带有一个参数的函数时,我认为这并不总是(或必然永远)有帮助。您必须运行一些基准测试才能看到。但是您也可以使用带有一个参数的函数来调用它!

fix (1 :)

是一个循环链表。使用 fix 的幼稚定义,它会变成一个无限的列表,随着结构的强制而懒惰地构建新的部分。

【讨论】:

    【解决方案3】:

    我相信已经有人问过这个问题,但我找不到答案。原因是第一个版本

    fix f = f (fix f)
    

    是递归函数,所以不能内联再优化。来自GHC manual

    例如,对于自递归函数,循环断路器只能是函数本身,因此始终忽略 INLINE pragma。

    但是

    fix f = let x = f x in x
    

    不是递归的,递归被移动到let绑定中,所以可以内联它。

    更新:我做了一些测试,虽然前一个版本不内联,而后者内联,但它似乎对性能并不重要。所以其他解释(堆上的单个对象与每次迭代创建一个)似乎更准确。

    【讨论】:

    • 我认为不涉及任何内联。
    • @AndrásKovács 正确。 fix 的两种变体都以不同的方式编译,一种通过let rec 绑定,另一种通过rec。 (-ddump-simpl)。
    猜你喜欢
    • 2023-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-07
    • 1970-01-01
    • 2017-01-10
    • 2013-12-18
    相关资源
    最近更新 更多