为什么Haskell(GHC)如此快速?
Haskell(与GHC
编译器) 比你想象的要快很多 。 正确使用,它可以接近低级语言。 (Haskellers最喜欢的做法是尝试在C的5%以内(甚至可以打败它,但这意味着你正在使用一个低效的C程序,因为GHC把C编译成Haskell)。我的问题是,为什么?
Haskell是声明式的,基于lambda演算。 机器体系结构显然是必不可less的,大致基于图灵机。 事实上,Haskell甚至没有具体的评估顺序。 另外,不用处理机器数据types,而是始终创build代数数据types。
最奇怪的是高阶function。 你可能会认为,即时创build函数并将其扔掉,会使程序变慢。 但是使用更高阶的函数实际上使得Haskell更快。 实际上,它似乎优化了Haskell代码,您需要使其更加优雅和抽象,而不是更像机器。 如果Haskell的更高级的function没有改进,那么它的性能甚至都不会影响它的性能。
对不起,如果这听起来简单,但这里是我的问题: 为什么Haskell(与GHC编译)如此之快,考虑到其抽象的性质和与物理机器的差异?
注意:我说C语言和其他命令式语言的原因有点类似于图灵机(但是不像Haskell类似于Lambda微积分),在命令式语言中,有一个有限数量的状态(又称行号) ,以及一个磁带(公羊),这样的状态和当前磁带决定如何做磁带。 请参阅维基百科条目, 图灵机等价物 ,以便从图灵机转换到计算机。
我认为这是一个有点意见的基础。 但我会尽力回答。
我同意迪特里希·埃普(Dietrich Epp)的观点:这是GHC快速组合的几件事情 。
首先,Haskell是非常高级的。 这使编译器能够执行积极的优化而不会破坏您的代码。
想想SQL。 现在,当我编写一个SELECT
语句时,它可能看起来像一个命令循环, 但它不是 。 它可能看起来像遍历该表中的所有行,试图find符合指定条件的行,但实际上 “编译器”(数据库引擎)可能正在做一个索引查找 – 它具有完全不同的性能特征。 但是由于SQL是如此高级的,“编译器”可以完全不同的algorithm,透明地应用多个处理器或者I / O通道或者整个服务器 ,等等。
我认为Haskell是一样的。 您可能会认为您刚刚要求Haskell将input列表映射到第二个列表,将第二个列表筛选到第三个列表中,然后计算结果的项目数。 但是你没有看到GHC在后台应用stream融合重写规则,把整个事物转换成一个严格的机器代码循环,这个循环把整个工作一次一遍地传递给数据,而没有分配 – 这种事情会手工编写是乏味,易出错和不可维护的。 这只是因为代码中缺less低级细节才可能实现。
另一种看待它的方法可能是…为什么Haskell 不应该快? 这是什么让它变慢?
这不是像Perl或JavaScript这样的解释性语言。 它甚至不是像Java或C#这样的虚拟机系统。 它一直编译到本机机器码,所以没有开销。
与OO语言[Java,C#,JavaScript …]不同,Haskell具有完整的types擦除[如C,C ++,Pascal …]。 所有的types检查只在编译时发生。 所以没有任何运行时types检查来减缓你的速度。 (对于这个问题,没有空指针检查,在Java中,JVM必须检查空指针并抛出一个exception,如果你遵守这个指针,Haskell就不必担心这个检查。
您认为“在运行时dynamic创build函数”听起来很慢,但如果仔细观察,实际上并没有这样做。 它可能看起来像你做的,但你没有。 如果你说(+5)
,那么这是硬编码到你的源代码。 它不能在运行时更改。 所以这不是一个真正的dynamicfunction。 即使curried函数实际上只是将参数保存到数据块中。 所有的可执行代码实际上都是在编译时存在的; 没有运行时的解释。 (与其他一些具有“eval函数”的语言不同)
想想帕斯卡。 它已经很老了,没有人再使用它了,但是没有人会抱怨Pascal很慢 。 有很多不喜欢的东西,但慢的不是其中之一。 Haskell实际上并没有像Pascal这么做,除了垃圾收集而不是手动内存pipe理。 而且不可变的数据允许对GC引擎进行几次优化[懒惰的评估稍微复杂化]。
我认为Haskell 看上去是先进的,高级的,高级的,大家都认为“哇,这真的很厉害, 一定是太慢了! ”但事实并非如此。 或者至less,这不符合你的期望。 是的,它有一个惊人的types系统。 但你知道吗? 这一切都发生在编译时。 通过运行,它消失了。 是的,它允许你用一行代码构造复杂的ADT。 但是你知道吗? ADT只是一个简单的普通C struct
的结合struct
。 而已。
真正的杀手是懒惰的评价。 当你的代码被严格/懒惰的时候,你可以编写一些高雅而又美丽的代码。 但是如果你弄错了这个东西,你的程序会慢几千倍 ,为什么会这样呢?
例如,我写了一个简单的小程序来计算每个字节出现在文件中的次数。 对于一个25KB的input文件,该程序需要20分钟的时间才能运行并吞下6千兆字节的RAM! 这太荒唐了! 但是后来我意识到问题出在哪里,加了一个单一的模式,运行时间降到了0.02秒 。
这就是Haskell出乎意料地缓慢的地方。 它肯定需要一段时间才能习惯它。 但是随着时间的推移,编写真正快速的代码变得更容易。
是什么让Haskell这么快? 纯度。 静态types。 懒惰。 但最重要的是,编译器可以在不破坏代码期望的情况下从根本上改变实现,这足够高级。
但我想这只是我的意见…
很长一段时间以来,function语言不可能很快 – 尤其是懒惰的function语言。 但是这是因为他们早期的实现本质上是被解释的而不是真正的编译。
第二波devise出现在图缩减的基础上,开辟了编译效率更高的可能性。 西蒙·佩顿·琼斯(Simon Peyton Jones)在他的两本书“function编程语言的 实现和实现函数式语言:一个教程” (Wadler和Hancock的部分,后者由David Lester编写)中写了这个研究。 (Lennart Augustsson也告诉我,前一本书的一个关键动机是描述了他的LML编译器没有被广泛评论的方式完成了它的编译)。
在这些作品中描述的图缩减方法背后的关键概念是,我们不把程序看作是一系列指令,而是将一个依赖图通过一系列局部缩减来评估 。 第二个关键的洞察是对这样的graphics的评估不需要被解释 ,而是graphics本身可以由代码构build 。 特别是,我们可以表示一个图的节点,而不是“值或操作码”和“操作的值”,而是作为调用时的函数返回所需的值。 第一次被调用时,它会向子节点询问它们的值,然后对它们进行操作, 然后用一个刚刚说“返回结果”的新指令覆盖自身。
这在后面的文章中有所描述,该文章为今天的GHC如何工作奠定了基础(尽pipe模调整了很多种不同的方式): “在库存硬件上实现懒惰函数语言:无骨头无标记G机器”。 。 GHC的当前执行模式更详细地logging在GHC Wiki中 。
所以有见解的是,我们认为机器如何工作的“基本”的“数据”和“代码”的严格区分不是它们必须如何工作,而是由我们的编译器施加的。 所以我们可以抛出它,并有代码(一个编译器),生成自修改代码(可执行文件),它可以很好地工作。
因此,事实certificate,虽然机器架构在某种意义上是必要的,但是语言可能以非常令人惊讶的方式映射到它们,看起来不像传统的C型stream控制,并且如果我们觉得足够低的话,这也可能是高效。
除此之外还有许多其他的优化,特别是纯度开放,因为它允许更大范围的“安全”转换。 什么时候以及如何运用这些变革,使事情变得更好而不是更糟糕,当然是一个经验性的问题,而在这个和其他许多小的select上,多年的工作都被纳入了理论工作和实践基准。 所以这当然也起到一个作用。 提供这种研究的一个很好的例子是“ 快速咖喱:推/input与评估/申请高阶语言”。
最后,应该指出的是,这个模型仍然由于间接导致了开销。 这是可以避免的情况下,我们知道是严格的做事是“安全的”,从而消除graphics间接。 在GHC Wiki中,再次详细记载了推断严格性/需求的机制。