为啥haskell中的递归列表这么慢?

Posted

技术标签:

【中文标题】为啥haskell中的递归列表这么慢?【英文标题】:Why the recursive list is so slow in haskell?为什么haskell中的递归列表这么慢? 【发布时间】:2012-07-18 08:24:27 【问题描述】:

我刚接触 Haskell,我在 Haskell 中定义了一个函数:

febs :: (Integral a)=> a -> a
febs n 
    | n<=0 =0
    | n==1 =1
    | n==2 =1
    | otherwise =febs(n-1)+febs(n-2)

但是,它运行得很慢,当我执行“febs 30”时,大约需要 10 秒, 我在 C++ 中执行相同的函数,它运行得非常快。

int febs(int n)

    if(n == 1 || n ==2)
    
        return 1;
    
    return febs(n-1)+febs(n-2);

有什么方法可以提高我的 haskell func 速度吗?

【问题讨论】:

该函数传统上称为“fib”或“fibs”,因为它为您提供斐波那契数。只是让我的迂腐远离:P。 【参考方案1】:

这是一个奇怪的比较,原因如下:

    你没有说你是否正在编译 Haskell 代码,或者有什么选项。如果您只是在 ghci 中运行它,那么它当然会很慢 - 您正在将解释代码与编译代码进行比较。

    您的 Haskell 代码是多态的,而您的 C++ 代码是单态的(也就是说,您使用了类型类 Integral a =&gt; a -&gt; a 而不是具体类型 Int -&gt; Int)。因此,您的 Haskell 代码比您的 C++ 代码更通用,因为它可以处理任意大的整数,而不是被限制在 Int 的范围内。编译器可能会优化掉这个,但我不确定。

如果我将以下代码放在文件 fib.hs 中

fibs :: Int -> Int
fibs n = if n < 3 then 1 else fibs (n-1) + fibs (n-2)

main = print (fibs 30)

并用ghc -O2 fib.hs 编译它,然后它运行得足够快,对我来说似乎是瞬时的。您应该尝试一下,看看它与 C++ 代码的比较。

【讨论】:

关于多态性:在这种情况下真的有效果吗?我猜这个函数在你使用它时会被内联或其他东西,但我不是专家。 (假设您进行了优化。)哦,假设它在调用站点具有Int 类型。仔细想想,这不是一个公平的假设。 其实,我不确定。我想你必须看看核心,但我没有足够的经验来做这件事! 实际上,我可以运行两个版本(带有两个签名)并比较运行时间。 “我”的意思是“你”,因为我必须启动 Linux :P。 @TikhonJelvis 它发挥了作用。由于它是递归的,因此不能内联。使用优化进行编译时(总是假设 GHC),如果它在与定义相同的源文件中单态使用,您将获得专业化。如果没有类型签名,在main 中将是Integer,所以比Int 慢一些。如果您在与它所使用的文件不同的文件中定义它,除非您将其设为-# INLINABLE #-,否则您会得到非常慢的多态代码。 @DanielFischer:这正是我想知道的。谢谢!【参考方案2】:

尝试使用优化进行编译。使用带有 -O2 的 GHC 7.4.1,您的程序运行得非常快:

$ time ./test 
832040

real    0m0.057s
user    0m0.056s
sys     0m0.000s

这是main = print (febs 30)


关于 Chris Taylor 的回答中的多态性考虑,这里是 febs 40 与 OP 的多态斐波那契函数:

$ time ./test 
102334155

real    0m5.670s
user    0m5.652s
sys     0m0.004s

这是一个非多态的,即用Int -&gt; Int替换OP的签名:

$ time ./test 
102334155

real    0m0.820s
user    0m0.816s
sys     0m0.000s

根据 Tikhon Jelvis 的评论,看看加速是否是由于将 Integer 替换为 Int 还是由于消除了多态性会很有趣。这又是同一个程序,除了 febs 根据 Daniel Fischer 的评论移动到一个新文件,以及 febs :: Integer -&gt; Integer

$ time ./test 
102334155

real    0m5.648s
user    0m5.624s
sys     0m0.008s

同样,febs 在不同的文件中,并且具有与最初相同的多态签名:

$ time ./test 
102334155

real    0m16.610s
user    0m16.469s
sys     0m0.104s

【讨论】:

感谢您测试多态与单态版本(我运行的是 Windows,所以运行时很痛苦!) Integral 类型默认为 Integer。我很好奇性能的变化是因为 IntegerInt 还是因为多态性也很重要。 您在与main 相同的文件中定义了函数,不是吗?在使用多态代码的单独文件中,febs 40 的运行时间为 15.5 秒,Integer -&gt; Integer 为 5.4 秒,Int -&gt; Int 为 0.86 秒。 @TikhonJelvis 这将回答您的问题。 gcc -O3 为 0.3 秒。 您必须在定义站点告诉 GHC 该函数应该专门用于某些类型(-# SPECIALISE foo :: Int -&gt; Int, Word -&gt; Integer #- [也接受美国拼写]),或者 - 需要 GHC >= 7 [可能是 7.2,不确定] - 使用-# INLINABLE foo #- pragma,它应该在接口文件中公开用于内联/优化/专门化的函数。然后(总是使用-O2)它会自动完成,至少如果嵌套不是太深[我可以想象如果你的调用树有 1000 个多态函数深度,优化器会放弃]。 一般来说是的。使用INLINABLE,在使用站点生成一个专门的版本。如果您在许多模块中使用一种特定类型的多态函数,每个模块都有自己的专用版本,那么最好使用SPECIALISE pragma 来减少(编译)代码大小。【参考方案3】:

你也可以这样写函数:

fibs = 0:1:zipWith (+) fibs (tail fibs)

它非常快,即使是大 'n' 立即执行:

Prelude> take 1000 fibs 

【讨论】:

是的,但这是渐近不同的。我认为真正的问题是将 C++ 与 Haskell 进行比较,因此使用不同的算法将会被淘汰。不过总的来说这是个好建议。 或者,更酷一点,像这样:fibs = 0 : scanl (+) 1 fibs 非常感谢,这是一个绝妙的解决方案,从这些代码中我可以感受到haskell的强大。 @Ed'ka 为什么中途停下来? fix ((0:) . scanl (+) 1). 前几天我看到一个非常好的:`let fibs@(t:fibs') = 1:zipWith (+) fibs fibs' in fibs

以上是关于为啥haskell中的递归列表这么慢?的主要内容,如果未能解决你的问题,请参考以下文章

Haskell中的懒惰和尾递归,为啥会崩溃?

为啥 Haskell 中的递归习语是“'n+1' and 'n'”而不是“'n' and 'n-1'”?

在 Haskell 中,为啥没有 TypeClass 用于可以像列表一样的东西?

为啥差异列表比 Haskell 中的常规连接更有效?

需要列表的原始长度作为 Haskell 中的“变量”,但它会随着递归而不断变化 [重复]

为啥使用 null 函数而不是 == [] 来检查 Haskell 中的空列表?