【问题标题】:How does haskell manage memory of recursive function callshaskell 如何管理递归函数调用的内存
【发布时间】:2020-02-07 06:37:44
【问题描述】:

我一直在研究一个问题,该问题从捕获我的函数结果中受益匪浅,在我的研究中,我遇到了this 文章。我惊讶于“递归记忆”部分的核心是多么简单:

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)

我觉得我了解如何它的工作原理,但如果我错了,请纠正我 - 这个函数保存了一个使用相同函数填充的列表。

困扰我的是我不明白为什么这行得通,最初我的印象是,一旦 haskell 评估一个函数,它就会释放用于在该函数内部存储变量的内存,但是在这里似乎如果列表的一部分是通过对该函数的一次调用进行评估的,那么这些值仍然可用于同一函数的另一次调用。

只是打字就让我头疼,因为我不明白为什么fib 2 的计算中使用的值应该在fib 3 或更好的是fib 100 的计算中可用?

我的直觉告诉我,这种行为有两个问题(我可能错了,但又不知道为什么):

  • 此函数的纯度我们正在评估一个调用,该调用使用并非来自此函数参数的变量
  • 内存泄漏不再确定 haskell 何时会从此列表中释放内存

【问题讨论】:

    标签: haskell recursion lazy-evaluation fibonacci memoization


    【解决方案1】:

    map fib [0..] 定义的列表被定义为函数定义的一部分,而不是在每次调用函数时创建。但是,由于懒惰,该列表仅在任何给定调用的必要时“实现”。

    假设您的第一个电话是memoized_fib 10。这将导致实际计算前 10 个斐波那契数并将其存储在内存中,并且它们将在程序执行期间保留在内存中。具有较小参数的后续调用不需要计算任何东西;具有较大参数的后续调用只需要计算列表中出现在最大现有元素之后的那些元素。

    【讨论】:

      【解决方案2】:

      我认为如果您将您的定义与此进行比较,会更容易理解:

      not_memoized_fib :: Int -> Integer
      not_memoized_fib m = map fib [0 ..] !! m
         where fib 0 = 0
               fib 1 = 1
               fib n = not_memoized_fib (n-2) + not_memoized_fib (n-1)
      

      上面的定义与你的基本相同,除了它需要一个显式参数m。它是前一个函数的所谓eta-expansion,在语义上等价于它。然而,在操作上,这会大大降低性能,因为这里没有进行记忆。

      为什么?好吧,您的函数定义了列表map fib [0..] before 采用(隐式)输入参数m,因此只有一个列表,对于所有m,我们稍后可以作为参数传递。相反,在not_memoized_fib 中,我们首先将m 作为输入,然后定义列表,使函数为每次调用not_memoized_fib 创建一个列表,从而破坏性能。

      更容易看出我们是否使用let 和 lambdas 而不是where。比较

      memoized :: Int -> Integer
      memoized = let
         list = map fib [0..]
         fib 0 = 0
         fib 1 = 1
         fib n = memoized (n-1) + memoized (n-2)
         in \m -> list !! m
         -- ^^ here we take m, after everything is defined,
      

      用它的让lambda (*)代码结构,到

      not_memoized :: Int -> Integer
      not_memoized = \m -> let
                  -- ^^ here we take m, before everything is defined, so
                  -- we define local bindings {list,fib} at every call
         list = map fib [0..]
         fib 0 = 0
         fib 1 = 1
         fib n = not_memoized (n-1) + not_memoized (n-2)
         in list !! m
      

      用 let inside lambda。

      在前一种情况下,周围只有一个列表,而在后一种情况下,每次调用都有一个列表。


      (*) 一个可搜索的术语。

      【讨论】:

      • 顺便说一句,apparently,带有 -O2 的旧 GHC 甚至可以将 not_memoized_fib 编译为记忆库。我们确定最新版本的功能吗?也许这与 CSE 的变化有关......
      • 使用 GHC 8.6.4 和 -O2memoized_fibnot_memoized_fib 都同样快。切换到last $ map fib [0..m] 会使它(非常)慢。
      猜你喜欢
      • 2012-09-10
      • 1970-01-01
      • 1970-01-01
      • 2015-05-09
      • 2012-11-26
      • 2021-06-24
      • 1970-01-01
      • 2011-02-14
      • 2019-02-28
      相关资源
      最近更新 更多