最近关于斐波那契闭式表达式(here和here)以及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
所以我的问题是:有人可以帮助我理解为什么第一个实现速度要快得多吗?算法的复杂性,开销还是完全不同的东西? (我检查了两个函数产生相同的结果)。谢谢!
答案 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)