【问题标题】:Why does a more general type affect runtime in Haskell?为什么更通用的类型会影响 Haskell 中的运行时?
【发布时间】:2015-02-25 07:10:59
【问题描述】:

考虑以下两种无限斐波那契数列的实现:

fibsA :: Num a => [a]
fibsA = 0:1:(zipWith (+) fibsA (tail fibsA))

fibsB :: [Integer]
fibsB = 0:1:(zipWith (+) fibsB (tail fibsB))

在 GHCI 中,执行 fibsB !! k 比执行 fibsA !! k 快​​得多。 特别是,fibsA 的值似乎在不断地重新计算(而不是记忆/存储)。

此外,当类型签名被省略时,GHCI 的:t 显示为[Integer],函数执行相应的操作。

此行为也发生在编译代码中 (ghc -O3 Fibs.hs)。

为什么IntegerNum a => a 快这么多?

【问题讨论】:

标签: haskell types ghc


【解决方案1】:

当您编写fibsA :: Num a => [a] 时,编译器会构造本质上的内容

fibsA :: NumDict a -> [a]

在哪里

data NumDict a = NumDict
    { (+)         :: a -> a -> a
    , (-)         :: a -> a -> a
    , (*)         :: a -> a -> a
    , negate      :: a -> a
    , abs         :: a -> a
    , signum      :: a -> a
    , fromInteger :: Integer -> a
    }

请注意,Num a 已从约束转变为函数的参数。 A typeclass is essentially just a lookup table for each type that implements the class。所以对于Num,默认情况下你会拥有

mkInteger_NumDict :: NumDict Integer
mkInteger_NumDict = NumDict
    { (+) = integer_plus
    , (-) = integer_minus
    , (*) = integer_mult
    , ...
    }

mkInt_NumDict     :: NumDict Int

mkFloat_NumDict   :: NumDict Float

mkDouble_NumDict  :: NumDict Double

当实例被解析时,这些会自动传递给使用类型类的函数。这意味着我们的函数fibsA 本质上接受了一个参数。当你从 GHCi 调用它时,默认规则会启动并选择 Integer,但由于它是这样调用的,它在内部看起来更像这样:

{-# RecordWildCards #-}  -- To reduce typing

fibsA :: NumDict a -> [a]
fibsA nd@(NumDict{..}) = fromInteger 0 : fromInteger 1 : zipWith (+) (fibsA nd) (tail $ fibsA nd)

你看到这个问题了吗?它仍然是递归的,但现在它必须在每一步都进行函数调用,从而降低了性能。如果你想让它变得非常快,聪明的程序员会做的

fibsA nd@(NumDict{..}) = fromInteger 0 : fromInteger 1 : zipWith (+) fibsA' (tail fibsA')
    where fibsA' = fibsA nd

这至少允许记忆。然而,haskell 二进制文件并不能真正在运行时执行这种优化,这发生在编译时。所以你最终得到的是一个较慢的递归函数。使用fibsB,您可以具体指定类型,它的类型签名没有多态约束。值fibsB 没有隐式或显式参数,因此当引用它时,它是指向内存中同一对象的指针。 fibsA 是指向函数的指针,因此当递归使用时,它会返回内存中的新对象,并且没有记忆。这就是为什么fibsBfibsA 快的原因,只有fibsB 得到优化,因为编译器不必使其适用于所有Num,仅适用于Integer

【讨论】:

  • 很好的解释!那个视频真的很有趣。
  • @WChargin 我看过几次,SPJ 很好地解释了类型类是多么简单,我觉得理解它们是如何实现的会让它们更有意义。
【解决方案2】:

除了@bheklilr的透彻解释:你也可以让fibsA快,如果你在函数内部执行列表共享,使其非递归(隐藏递归):

fibsA' :: Num a => [a]
fibsA' = 
  let f = 0:1:(zipWith (+) f (tail f))
  in f

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-27
    • 2019-02-10
    • 1970-01-01
    • 1970-01-01
    • 2016-10-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多