Performance 为什么哈斯克尔(GHC)跑得这么快?

Performance 为什么哈斯克尔(GHC)跑得这么快?,performance,haskell,ghc,higher-order-functions,lambda-calculus,Performance,Haskell,Ghc,Higher Order Functions,Lambda Calculus,Haskell(带有GHC编译器)是一个。如果使用得当,它可以接近低级语言。(Haskeller最喜欢做的一件事是尝试获得C的5%以内(甚至超过它,但这意味着你使用的是一个低效的C程序,因为GHC将Haskell编译为C)。)我的问题是,为什么 Haskell是声明性的,基于lambda演算。机器架构显然是必不可少的,大体上是基于图灵机器的。事实上,Haskell甚至没有具体的评估顺序。此外,您不需要处理机器数据类型,而是始终使用代数数据类型 最奇怪的是高阶函数。您可能会认为,在运行中创建函数,

Haskell(带有
GHC
编译器)是一个。如果使用得当,它可以接近低级语言。(Haskeller最喜欢做的一件事是尝试获得C的5%以内(甚至超过它,但这意味着你使用的是一个低效的C程序,因为GHC将Haskell编译为C)。)我的问题是,为什么

Haskell是声明性的,基于lambda演算。机器架构显然是必不可少的,大体上是基于图灵机器的。事实上,Haskell甚至没有具体的评估顺序。此外,您不需要处理机器数据类型,而是始终使用代数数据类型

最奇怪的是高阶函数。您可能会认为,在运行中创建函数,并将其丢弃,会使程序变得更慢。事实上,要优化Haskell代码,您需要使其更加优雅和抽象,而不是像机器一样。如果Haskell的更高级的功能不改进的话,它们似乎都不会影响它的性能

抱歉,如果这听起来太过分了,但我的问题是:考虑到Haskell(用GHC编译)的抽象性质和与物理机器的差异,为什么它会这么快?


注:我之所以说C和其他命令式语言与图灵机有些相似(但没有达到Haskell与Lambda演算相似的程度),是因为在命令式语言中,有有限数量的状态(也称为行号)以及磁带(ram),以使状态和当前磁带决定对磁带执行的操作。有关从图灵机器到计算机的过渡,请参见维基百科条目。

我同意迪特里希·埃普的观点:GHC的快速发展是由多种因素共同作用的结果

首先也是最重要的是,Haskell的水平非常高。这使编译器能够在不破坏代码的情况下执行积极的优化

想想SQL。现在,当我编写一个
SELECT
语句时,它可能看起来像一个命令循环,但事实并非如此。它可能看起来像是在该表中的所有行上循环,试图找到与指定条件匹配的行,但实际上“编译器”(DB引擎)可能正在执行索引查找,这具有完全不同的性能特征。但由于SQL是如此高级,“编译器”可以替代完全不同的算法,透明地应用多个处理器或I/O通道或整个服务器,等等

我认为哈斯克尔也是一样。您可能认为您只是要求Haskell将输入列表映射到第二个列表,将第二个列表过滤到第三个列表,然后计算结果中的项目数。但您没有看到GHC在幕后应用流融合重写规则,将整个过程转换为一个紧密的机器代码循环,在不分配任何数据的情况下通过一次数据传递完成整个工作——这种事情会很乏味,容易出错,而且不可手工维护。这只有在代码中缺乏底层细节的情况下才有可能实现

另一种看待它的方式可能是…为什么哈斯凯尔不快?它做了什么使它变慢

它不是像Perl或JavaScript那样的解释语言。它甚至不像Java或C#那样是一个虚拟机系统。它一直编译到本机代码,因此没有开销


不像OO语言[java,c],javascript……,Haskell有全类型擦除[C,C++,Pascal…]。所有类型检查仅在编译时进行。因此,也没有运行时类型检查来降低速度。(就此而言,没有空指针检查。比如说,在Java中,JVM必须检查空指针,如果您遵守一个异常,JVM必须抛出一个异常。Haskell不必费心进行该检查。)

你说“在运行时动态创建函数”听起来很慢,但如果你仔细观察,你实际上并没有这样做。看起来像是你做的,但你没有。如果你说
(+5)
,那么,这是硬编码到源代码中的。它不能在运行时更改。所以它不是一个真正的动态函数。甚至curry函数实际上也只是将参数保存到数据块中。所有可执行代码实际上都存在于编译时;没有运行时解释。(与其他一些具有“eval函数”的语言不同。)

想想帕斯卡。它很旧,没有人真正使用它了,但没有人会抱怨帕斯卡速度慢。它有很多不喜欢的地方,但缓慢并不是其中之一。Haskell除了进行垃圾收集而不是手动内存管理之外,并没有做与Pascal不同的事情。不可变的数据允许对GC引擎进行多次优化(这会使评估变得有些复杂)

我认为哈斯凯尔看起来先进、成熟、高水平,每个人都认为“哦,哇,这真的很强大,它一定非常慢!”但事实并非如此。或者至少,这不是你所期望的。是的,它有一个惊人的打字系统。但是你知道吗?这一切都发生在编译时。在运行时,它已经消失了。是的,它允许您用一行代码构造复杂的ADT。但是你知道吗?ADT只是
struct
s的普通C
union
。没别的了

真正的杀手是懒惰的评估。当您正确地理解了代码的严格性/惰性时,您可以编写出仍然优雅而漂亮的速度极快的代码。但是如果你把这些东西弄错了,你的程序就会慢上千倍,这是为什么会发生的,这真的不明显

例如,我编写了一个小程序来计算每个字节在文件中出现的次数。对于25KB的输入文件,程序运行了20分钟,占用了6GB内存!那太荒谬了!!但后来我意识到了问题所在,添加了一个单一的爆炸模式