【问题标题】:Fibonacci's closed-form expression, the ST monad, and Haskell斐波那契的封闭式表达式、ST monad 和 Haskell
【发布时间】:2011-05-22 12:02:32
【问题描述】:

最近关于斐波那契封闭式表达式的两个问题(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

所以我的问题是:谁能帮我理解为什么第一个实现这么快?是算法复杂性、开销还是其他什么? (我检查了两个函数是否产生相同的结果)。谢谢!

【问题讨论】:

  • 你总是可以通过不使用 Rational 来加速第一个。

标签: haskell fibonacci


【解决方案1】:

您在这里比较了非常不同的版本。为了公平起见,这里有一个等效于您提供的 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 版本一样好或坏(这里都是10s)。运行时很可能由所有 Integer 添加控制,因此开销太低而无法测量。

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

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

      第一个是O(1);

      第二个是O(n)

      【讨论】:

      • 第一个的复杂度不是O(1)。求幂需要O(log n)乘法,由于最终结果有Theta(n)位,所以整体复杂度不能低于O(n)。
      • 我知道我错了。你如何计算第二部分,找到下限?
      猜你喜欢
      • 2019-04-14
      • 2018-07-03
      • 2017-12-06
      • 2017-01-10
      • 2015-01-06
      • 1970-01-01
      • 2011-02-17
      • 1970-01-01
      相关资源
      最近更新 更多