Haskell Fibonacci:Tail-Recursive比经典的懒惰列表方法慢?

时间:2016-10-10 13:12:06

标签: haskell recursion optimization tail-recursion microbenchmark

免责声明:可能会有一些可疑的Haskell。

我已经实现了两个版本的Fibonacci,一个是线性和尾递归(包括一个带有变种的版本),另一个使用经典的懒惰列表方法:

{-# LANGUAGE BangPatterns #-}

-- Linear
fibLinear :: Int -> Integer
fibLinear 0 = 0
fibLinear 1 = 1
fibLinear x = fibLin' 2 1 2
  where
      fibLin' i y z | i == x = y
      fibLin' i y z          = fibLin' (i+1) z (z+y)

-- Linear banged
fibLinearBang :: Int -> Integer
fibLinearBang 0 = 0
fibLinearBang 1 = 1
fibLinearBang x = fibLin' 2 1 2
  where
      fibLin' (!i) (!y) (!z) | i == x = y
      fibLin' (!i) (!y) (!z)          = fibLin' (i+1) z (z+y)

-- Haskeller classic lazy
fibClassic :: Int -> Integer
fibClassic n = fib' !! n
    where fib' = 0:1:zipWith (+) fib' (tail fib')

使用criterion对其进行基准测试(code)时,我得到了一些不直观的结果:{30}对fibLinear采用443.0 ns,fibLinearBang采用279.5 ns,而对于73.51 ns为fibClassic。这很奇怪,因为我会认为fibLinear& fibLinearBang可以优化为循环或跳转等。

我唯一的猜测是,fib'中的fibClassic可能会被记忆为常量列表,尽管它位于函数定义中,以便后续的函数调用重用相同的列表进行查找。

有人能给我一个明确的基准测试结果吗?

对于那些感兴趣的人,我已将项目放在Github

1 个答案:

答案 0 :(得分:-1)

tl; dr:fib'确实在调用中重复使用

感谢评论者,我了解到full-laziness不是(仅仅)用于描述Haskell如何评估的术语,而是一个实际的编译器优化术语,默认情况下是一个,但可以禁用(使用-fno-full-laziness GHC选项标志。

使用该标志重新运行基准测试,我们看到表格已转为:

  • fibLinearBang 259.8 ns
  • fibClassic 738.4 ns

这个表现以及this guide on GHC compiler options是非常有说服力的证据,证明fib'确实被转化为类似于顶层引用的东西,它在不同的调用中被重用(并且是一个列表,被记忆)。方法。

该指南和paper it links to describing let-lifting都承认放松可能会导致内存占用率增加,这对我来说有点可怕(不要使用fibClassic写一个虚拟即服务把它暴露在互联网上...),但我可能会来到它...