Performance 哈斯克尔基本性能

Performance 哈斯克尔基本性能,performance,haskell,erlang,Performance,Haskell,Erlang,在开始之前,我想说的是,我已经知道,在绝大多数用例中,性能并不是“全部结束”或不相关。我也知道“如果性能是一个巨大的问题,请使用填空(C、汇编等)”,所以我不需要包含我刚才所说内容的答案。为了我们的目的,假设1)我只是在智力上好奇,或者2)我有一些其他相关的原因来探索这一点 无论如何,虽然我对函数式编程(Erlang等)或递归和单赋值编程(Prolog)并不陌生,但我对Haskell非常陌生。我只是想得到一些基本的基准测试,看看它是如何执行基本任务的,比如一遍又一遍地调用函数,遍历列表等等 为了

在开始之前,我想说的是,我已经知道,在绝大多数用例中,性能并不是“全部结束”或不相关。我也知道“如果性能是一个巨大的问题,请使用填空(C、汇编等)”,所以我不需要包含我刚才所说内容的答案。为了我们的目的,假设1)我只是在智力上好奇,或者2)我有一些其他相关的原因来探索这一点

无论如何,虽然我对函数式编程(Erlang等)或递归和单赋值编程(Prolog)并不陌生,但我对Haskell非常陌生。我只是想得到一些基本的基准测试,看看它是如何执行基本任务的,比如一遍又一遍地调用函数,遍历列表等等

为了测试一次又一次地调用一个函数的性能(我想要一个实际上什么都不做的函数,即“no op”,但却不知道如何让Haskell执行这样的构造),我写了以下内容:

count 0 = 0
count x = 1 + count (x-1)

main = print count 100000000   -- tried various values for this integer.
我将其与此Erlang程序进行了比较:

count(0) -> 0 ;
count(I) -> 1 + count(I-1)
令我惊讶的是,Erlang程序在这两个程序的多次运行中运行得更快。事实上,它(至少表面上)比这更糟糕,因为即使是遍历x元素列表(而不是简单地调用函数x次)的Erlang变体程序也比上面的Haskell版本运行得更快。另外,我使用的是Haskell代码(ghc-make-O3-rtsopts)的编译版本,而不是Erlang的字节码解释器(无Hipe)

我对哈斯克尔的了解不够(现在仍然不够),不知道从哪里开始,但我的第一个猜测是怀疑懒惰。快速查看一些在线文档后,我将主要内容更改为:

main = print $! count 100000000
它似乎加快了速度,但Erlang版本仍然更快,而且无论如何,我不确定我是否做了足够严格的工作来产生足够的效果,是否可以做更多的工作,是否我找错了方向,是否还有其他问题,等等

根据我多年来读到的所有内容,我认为对于大多数“通用任务”,编译Haskell通常应该比Erlang更快,尽管这一点可能随着并发性的增加而变得越来越不正确。有人能解释一下这些结果吗?“Shed light”可以重写我的程序,使用不同的编译标志,解释一些事情,等等

编辑:我做了两件事,这两件事都加速了Haskell计划。我做的第一件事是将此类型信息添加到函数中:

count :: Int -> Int
这使Haskell版本的性能达到了Erlang版本的水平。我做的第二件事是删除一个附加项:

count 0 = 0
count x = count (x-1)

这导致Haskell版本优于Erlang版本(为了公平起见,我也调整了Erlang版本);然而,我确实想知道为什么消除加法会产生这种效果,因为我不相信Erlang是一种数学计算野兽。我还想知道Haskell的编译器加上它的惰性是否只是绕过所有函数调用,直接跳到答案。

第一个版本的
count
和第二个版本(总是返回0的版本)之间的一个重要区别是后者是尾部递归的。第一个函数的尾部递归版本是

count' :: Int -> Int
count' n = go n 0 where
    go 0 !acc = acc
    go n !acc = go (n - 1) (acc + 1)
不到三分之一的时间

函数签名在这里很重要,因为没有它,GHC将函数类型默认为
Integer->Integer
,这意味着它必须处理任意精度的整数,而不是
Int
s


(GHC完全有可能优化您的第二个版本,使其只返回一个常量,但我没有对此进行研究。尽管尾部递归本身会产生显著的性能差异。)

我想我不知道Haskell中的尾部递归是什么样子。该程序的第一个版本应该是用Erlang和我使用过的其他语言(Prolog等)编写的尾部递归程序。我必须承认我不理解你写的Haskell代码,因此我还有更多的研究要做。谢谢我真的不明白你的第一个版本怎么会在任何语言中都是尾部递归的。函数是递归的,如果它做的最后一件事是调用它自己。您的版本在调用自身后进行添加。在这里,您基本上执行了两个fjh优化:尾部递归和bang模式,以加强严格性而不是懒散性。你也应该在你的回答中解释这一点。没有严格限制的尾部递归不是一种优化。使事物尾部递归本身没有任何价值。如果累加器在递归过程中增长,则仍会分配大量空间。堆栈与堆空间在Haskell中几乎不重要,因为堆栈实际上是在堆上分配的。在这种情况下,使累加器严格可确保其空间恒定,因此这是一种优化(在一个简单的情况下,例如
count'
,GHC优化器甚至可以在没有注释的情况下计算出来)。请注意,
print$不表示任何内容,因为为了打印,必须对某些内容进行评估。