为啥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 => a -> a
而不是具体类型 Int -> 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 -> Int
替换OP的签名:
$ time ./test
102334155
real 0m0.820s
user 0m0.816s
sys 0m0.000s
根据 Tikhon Jelvis 的评论,看看加速是否是由于将 Integer
替换为 Int
还是由于消除了多态性会很有趣。这又是同一个程序,除了 febs
根据 Daniel Fischer 的评论移动到一个新文件,以及 febs :: Integer -> 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
。我很好奇性能的变化是因为 Integer
与 Int
还是因为多态性也很重要。
您在与main
相同的文件中定义了函数,不是吗?在使用多态代码的单独文件中,febs 40
的运行时间为 15.5 秒,Integer -> Integer
为 5.4 秒,Int -> Int
为 0.86 秒。 @TikhonJelvis 这将回答您的问题。 gcc -O3 为 0.3 秒。
您必须在定义站点告诉 GHC 该函数应该专门用于某些类型(-# SPECIALISE foo :: Int -> Int, Word -> 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 中的递归习语是“'n+1' and 'n'”而不是“'n' and 'n-1'”?
在 Haskell 中,为啥没有 TypeClass 用于可以像列表一样的东西?