【问题标题】:GHC optimizationGHC 优化
【发布时间】:2017-05-23 15:08:59
【问题描述】:

下面的两个 Haskell 函数的区别似乎仅在于索引变量是隐式还是显式,但性能上的差异是两个数量级。

此函数计算 mfib 30 大约需要 0.03 秒:

let mfib = (map fib [0..] !!)
  where
    fib 0 = 0
    fib 1 = 1
    fib x = mfib (x-1) + mfib (x-2)

这个函数对于 mfib 30 大约需要 3 秒:

let mfib i = map fib [0..] !! i
  where
    fib 0 = 0
    fib 1 = 1
    fib x = mfib (x-1) + mfib (x-2)

我猜它与 GHC 内联规则有关,并且一直在尝试添加内联/非内联编译指示以获得匹配的性能。

编辑:我了解如何使用惰性列表查找来记忆 fib 函数以及为什么 fib 的传统定义非常慢。我期待记忆化在第二个函数和第一个函数中都能工作,但不明白为什么它不是。

【问题讨论】:

  • 关键是memoization。见here

标签: haskell optimization ghc


【解决方案1】:

查看脱糖代码时更容易理解这些差异,因此这里是两个函数的部分脱糖版本。

let mfib = let fib 0 = 0
               fib 1 = 1
               fib x = mfib (x-1) + mfib (x-2)
           in (!!) (map fib [0..])

let mfib = \i ->
               let fib 0 = 0
                   fib 1 = 1
                   fib x = mfib (x-1) + mfib (x-2)
               in map fib [0..] !! i

请注意,在第二个程序中,表达式map fib [0..] 出现在\i -> ... 中,因此它会(通常不进行优化)对i 的每个值进行评估。见When is memoization automatic in GHC Haskell?

【讨论】:

  • 我在 repl 上做了一些测试,试图确认这一点,似乎是正确的。我还查看了@alexey-radkov 提供的关于记忆化的链接,并建议它与第一个函数是单态的,因此在调用之间共享,而第二个函数是多态的,但我无法确认这一点,例如通过将map 重新定义为单态。
  • TL;DR 因为map fib [0..] 在 lambda 中,所以它不是共享的,而是在(递归)调用之间收集垃圾。对吗?
  • @Mikkel 对,原因是(尽管在这种情况下不是)它可能依赖于i,然后无法共享。 (这通常是一种无需先进行脱糖即可查看共享内容的好方法。)
【解决方案2】:

不,这与内联无关。不同之处在于mfib = (map fib [0..] !!) 没有参数。当然,它仍然是一个函数,但预先评估该函数不需要传递任何参数。特别是,评估此mfib 将生成fib 列表,以便它可以重复用于所有索引。

OTOH,mfib i = map fib [0..] !! i 意味着整个 where 块只有在您实际传递参数 i 时才会被考虑。

只有当您一次又一次地多次评估一个函数时,两者才会有所不同。不幸的是,对于第二个版本,函数本身的递归已经一次又一次地调用它!所以mfib (x-1) + mfib (x-2) 然后需要完成mfib (x-1) 的全部工作,然后再次完成mfib (x-2) 的全部工作。所以mfib n 的计算成本是mfib (n-1) 的两倍多,因此mfibO (2n)。 p>

这非常浪费,因为mfib (x-2) 中的大部分术语也已经在mfib (x-1) 中,并且可以简单地重复使用。嗯,这正是您的第一个版本所做的,因为它一次性计算 fib 列表并用于所有索引,因此评估 mfib (x-1) 已经完成了大部分工作,然后可以简单地由 mfib (x-2) 重新读取,从而减少多项式的复杂度。

【讨论】:

  • 为什么重新评估where 块的一点额外解释:这是因为iwhere 块的闭包中。如果您将其写为let mfib = \i -> map fib [0..] !! i where ...,它将与 eta-contracted 版本一样快。也就是说,我很惊讶 GHC 没有发现机会应用完全惰性转换并将 fib 浮动到活页夹之外。
  • @BenjaminHodgson 我确实尝试将 i 放入 lambda 中,但这没有区别——记忆化仍然没有“工作”。
猜你喜欢
  • 2011-09-22
  • 2012-06-08
  • 2018-11-30
  • 2016-06-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多