Fibonacci的封闭形式表达式,ST monad和Haskell

时间:2011-05-22 12:02:32

标签: haskell fibonacci

最近关于斐波那契闭式表达式(herehere)以及HaskellWiki's page about the ST monad的两个问题促使我尝试比较两种计算斐波纳契数的方法。

第一个实现使用闭合形式表达式和hammar's answer here中所见的有理数(其中Fib是抽象形式a + b *√5的数字的数据类型):

fibRational :: Integer -> Integer
fibRational n = divSq5 $ phi^n - (1-phi)^n
  where
    phi = Fib (1/2) (1/2)
    divSq5 (Fib 0 b) = numerator b

第二个实现是来自HaskellWiki关于ST monad的页面,为了避免堆栈溢出,必须增加一些严格性:

fibST :: Integer -> Integer
fibST n | n < 2 = n
fibST n = runST $ do
    x <- newSTRef 0
    y <- newSTRef 1
    fibST' n x y
  where
    fibST' 0 x _ = readSTRef x
    fibST' !n x y = do
      x' <- readSTRef x
      y' <- readSTRef y
      y' `seq` writeSTRef x y'
      x' `seq` writeSTRef y (x'+y')
      fibST' (n-1) x y

作为参考,这里也是我用于测试的完整代码:

{-# LANGUAGE BangPatterns #-}

import Data.Ratio
import Data.STRef.Strict
import Control.Monad.ST.Strict
import System.Environment

data Fib =
  Fib !Rational !Rational
  deriving (Eq, Show)

instance Num Fib where
  negate (Fib a b) = Fib (-a) (-b)
  (Fib a b) + (Fib c d) = Fib (a+c) (b+d)
  (Fib a b) * (Fib c d) = Fib (a*c+5*b*d) (a*d+b*c)
  fromInteger i = Fib (fromInteger i) 0
  abs = undefined
  signum = undefined

fibRational :: Integer -> Integer
fibRational n = divSq5 $ phi^n - (1-phi)^n
  where
    phi = Fib (1/2) (1/2)
    divSq5 (Fib 0 b) = numerator b

fibST :: Integer -> Integer
fibST n | n < 2 = n
fibST n = runST $ do
    x <- newSTRef 0
    y <- newSTRef 1
    fibST' n x y
  where
    fibST' 0 x _ = readSTRef x
    fibST' !n x y = do
      x' <- readSTRef x
      y' <- readSTRef y
      y' `seq` writeSTRef x y'
      x' `seq` writeSTRef y (x'+y')
      fibST' (n-1) x y

main = do
  (m:n:_) <- getArgs 
  let n' = read n
      st = fibST n'
      rt = fibRational n'
  case m of
    "st" -> print st
    "rt" -> print rt
    "cm" -> print (st == rt)

现在事实证明ST版本明显比封闭版本慢,尽管我不是百分之百确定原因:

# time ./fib rt 1000000 >/dev/null
./fib rt 1000000 > /dev/null  0.23s user 0.00s system 99% cpu 0.235 total

# time ./fib st 1000000 >/dev/null
./fib st 1000000 > /dev/null  11.35s user 0.06s system 99% cpu 11.422 total

所以我的问题是:有人可以帮助我理解为什么第一个实现速度要快得多吗?算法的复杂性,开销还是完全不同的东西? (我检查了两个函数产生相同的结果)。谢谢!

3 个答案:

答案 0 :(得分:4)

首先,这两个实现使用两种非常不同的算法,这些算法具有不同的渐近复杂度(很好,取决于整数运算的复杂性)。 其次,st实现使用引用。参考文献(相对)ghc缓慢。 (因为更新引用需要GC写屏障,因为分代垃圾收集器。)

因此,您要比较算法和实现技术两者不同的两个函数。 您应该重写第二个不使用引用,这样您就可以只比较算法。或者重写第一个使用引用。但是,为什么在错误的时候使用引用呢? :)

答案 1 :(得分:4)

您在这里比较非常不同的版本。为了公平起见,这里的实现等同于您提供的ST解决方案,但在纯Haskell中:

fibIt :: Integer -> Integer
fibIt n | n < 2 = n
fibIt n = go 1 1 (n-2)
  where go !_x !y  0 = y
        go !x  !y  i = go y (x+y) (i-1)

这个版本的表现与ST版本(这里都是10个)一样好或坏。运行时很可能由所有Integer次加法支配,因此开销太低而无法衡量。

答案 2 :(得分:0)

您可以比较算法的复杂性。

第一个是O(1);

第二个是O(n)