为什么Haskell(GHC)这么快?


246

Haskell(使用GHC编译器)比您期望的要快得多。正确使用它可以接近低级语言。(Haskellers最喜欢做的事情是尝试获得5%的C(甚至击败它),但这意味着您正在使用效率低下的C程序,因为GHC将Haskell编译为C。)我的问题是,为什么?

Haskell是声明性的,基于lambda演算。机器架构显然必须基于图灵机。实际上,Haskell甚至没有特定的评估顺序。另外,无需处理机器数据类型,而是始终创建代数数据类型。

最奇怪的是高阶函数。您可能会认为,即时创建函数并将其扔掉会使程序变慢。但是使用高阶函数实际上会使Haskell更快。实际上,为了优化Haskell代码,您似乎需要使其更优雅,更抽象,而不是像机器一样。如果不能改善Haskell的更高级功能,它们似乎都不会影响它的性能。

抱歉,这听起来很抱歉,但这是我的问题:考虑到Haskell的抽象性质以及与物理机器的区别,为什么Haskell(与GHC编译)这么快?

注意:我说C和其他命令式语言在某种程度上类似于图灵机的原因(但在某种程度上来说,Haskell与Lambda微积分不一样)是,在命令式语言中,您具有有限数量的状态(即行号)以及Tape(撞锤),以便状态和当前磁带决定对磁带执行什么操作。从图灵机到计算机的过渡,请参阅Wikipedia条目,图灵机等效项。


27
“因为GHC将Haskell编译为C”-事实并非如此。GHC有多个后端。最老的(但不是默认值)是C生成器。它的确为IR生成了Cmm代码,但这并不是您通常期望的“编译为C”。(downloads.haskell.org/~ghc/latest/docs/html/users_guide/...
viraptor

19
我强烈建议阅读Simon G.的主要实现者Simon Payton Jones 的《函数式编程语言的实现》,它将回答您的许多问题。
Joe Hillenbrand

94
为什么?25年的努力。
2013

31
“尽管可能有事实答案,但它只能征求意见。” -这是关闭问题的最糟糕的原因。因为它可能有一个很好的答案,但它也有可能吸引低质量的人。!我碰巧对学术研究和某些发展发生了一个很好的,历史的,事实的答案。但是我不能发布它,因为人们担心这个问题还会吸引低质量的答案。再次,。
sclv

7
@cimmanon我需要一个月或几篇博客文章来介绍功能编译器如何工作的细节。我只需要一个SO答案就可以粗略地概述草图机如何在库存硬件上干净地实现,并指向相关资源以供进一步阅读...
sclv 2016年

Answers:


264

我同意Dietrich Epp的观点:这是使GHC快速的几件事的结合。

首先,Haskell是非常高级的。这使编译器可以执行积极的优化而不会破坏您的代码。

考虑一下SQL。现在,当我编写一条SELECT语句时,它看起来像是一个命令式循环,但不是。它可能看起来像它遍历该表中的所有行试图找到指定的条件匹配的那一种,但实际上在“编译”(数据库引擎),可以查找,而不是做一个指数-该指数具有完全不同的性能特征。但是由于SQL如此高级,“编译器”可以替代完全不同的算法,透明地应用多个处理器或I / O通道或整个服务器,等等。

我认为Haskell是相同的。您可能认为您只是要求Haskell将输入列表映射到第二个列表,将第二个列表过滤到第三个列表,然后计算产生的项目数。但是您没有看到GHC在后台应用流融合重写规则,而是将整个过程转换成一个紧密的机器代码循环,从而无需分配即可在一次数据传递中完成整个工作-那种事情会繁琐,容易出错且无法手动书写。由于代码中缺少底层细节,因此这仅是真正可能的。

另一种看待它的方式可能是…… Haskell 为什么应该快?它应该做什么使它变慢?

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

与OO语言[Java,C#,JavaScript ...]不同,Haskell具有完整类型擦除(例如C,C ++,Pascal ...)。所有类型检查仅在编译时进行。因此,也没有运行时类型检查来减慢您的速度。(就这一点而言,没有空指针检查。例如,在Java中,JVM必须检查空指针并在您引用一个空指针时抛出异常。Haskell不必费心该检查。)

您说“在运行时动态创建函数”听起来很慢,但是如果仔细看,实际上并没有这样做。可能看起来像您,但事实并非如此。如果您说(+5)好的,那就是硬编码到您的源代码中。它不能在运行时更改。因此,它并不是真正的动态功能。即使是咖喱函数也实际上只是将参数保存到数据块中。实际上,所有可执行代码都在编译时存在。没有运行时解释。(与其他具有“评估功能”的语言不同。)

想想帕斯卡。它很旧,没有人真正使用它,但是没人会抱怨Pascal 速度。关于它,有很多事情让人不喜欢,但慢速并不是其中之一。Haskell实际上并没有做与Pascal不同的事情,除了具有垃圾回收而不是手动内存管理。不变的数据允许对GC引擎进行多项优化(这种惰性评估随后会使情况有些复杂)。

我认为问题是Haskell 看起来很先进,高级,而且每个人都认为“哦,哇,这真的很强大,它一定非常慢! ”但是事实并非如此。或者至少,它不是您期望的那样。是的,它有一个了不起的类型系统。但是你知道吗?这一切都发生在编译时。在运行时,它已经消失了。是的,它允许您使用一行代码来构造复杂的ADT。但是你知道吗?一个ADT只是一个简单的普通的C unionstruct秒。而已。

真正的杀手is是懒惰的评估。当您正确地执行了代码的严格性/惰性时,您可以编写出愚蠢的快速代码,仍然优雅而美丽。但是,如果您弄错了这些东西,您的程序就会变慢数千倍,而发生这种情况的确不是很明显。

例如,我编写了一个简单的小程序来计算每个字节出现在文件中的次数。对于25KB的输入文件,该程序需要20分钟的运行时间并吞没了6 GB的RAM!太荒谬了!但是后来我意识到了问题所在,添加了一个爆炸样式,运行时间降至0.02秒

是Haskell意外缓慢的地方。而且它需要一段时间才能习惯。但是随着时间的流逝,编写真正快速的代码变得更加容易。

是什么让Haskell如此之快?纯度。静态类型。懒惰。但最重要的是,它具有足够高的层次,以使编译器可以从根本上更改实现而不会超出代码的期望。

但是我想那只是我的看法...


13
@cimmanon我不认为它纯粹是基于观点的。其他人可能想要回答这个有趣的问题。但是我想我们会看到其他选民的想法。
MathematicalOrchid

8
@cimmanon-该搜索仅给出一个半线程,并且它们都与审核审核有关。对该线程的最高评价是“请停止审核您不了解的内容”。我建议,如果有人认为答案肯定太广泛了,那么他们会为之惊讶并享受答案,因为答案并不是太广泛。
sclv

34
“例如,在Java中,JVM必须检查空指针并在引用一个空指针时抛出异常。” Java的隐式null检查(大部分)是无成本的。Java实现可以并且确实利用虚拟内存将空地址映射到丢失的页面,因此,取消引用空指针会触发CPU级别的页面错误,Java会将页面错误作为高级异常捕获并抛出。因此,大多数null检查都是由CPU中的内存映射单元免费完成的。
Boann

4
@cimmanon:也许是因为Haskell用户似乎是唯一一个实际上是一群友好的,开放思想的社区……您认为这是个“玩笑”……,而不是纳粹统治者的狗食狗社区。尽可能多地互相撕裂对方……这似乎是您认为的“正常”情况。
Evi1M4chine

14
@MathematicalOrchid:您是否拥有需要20分钟才能运行的原始程序的副本?我认为研究为什么这么慢会很有启发性。
乔治,

79

长期以来,人们一直认为功能语言不能很快-尤其是懒惰的功能语言。但这是因为它们的早期实现本质上是经过解释的,而不是真正经过编译的。

基于图归约的第二波设计浪潮涌现,为更有效的编译开辟了可能性。西蒙·佩顿·琼斯(Simon Peyton Jones)在他的两本书《函数式编程语言的实现函数式语言的实现》中写了一篇关于该研究的文章:一个教程(前者由Wadler和Hancock撰写,后者由David Lester撰写)。(Lennart Augustsson还告诉我,前本书的一个主要动机是描述其LML编译器(未受到广泛评论)完成其编译的方式)。

在这些工作中描述的图约简方法背后的关键概念是,我们不将程序视为一系列指令,而是将依赖图通过一系列局部约简进行评估。第二个主要见解是,无需解释这种图形的评估,而是可以用代码构建图形本身。特别是,我们可以将图的节点表示为不是“值或'操作码'以及要对其进行操作的值”,而是表示为在调用时返回所需值的函数。第一次调用它时,它会向子节点询问其值,然后对其进行操作,然后用一条新指令覆盖自身,该指令仅显示“返回结果。

在稍后的一篇文章中对此进行了描述,该文章为GHC仍如何工作提供了基础(尽管对许多各种调整进行了模数化):“在库存硬件上实现惰性功能语言:无脊椎无标签G机”。GHC的Wiki上详细记录了GHC的当前执行模型。

因此,洞察力在于,我们认为“数据”和“代码”对机器如何工作的“根本性”严格区分不是它们必须如何工作,而是由我们的编译器强加的。因此,我们可以将其排除在外,并拥有生成自我修改代码(可执行文件)的代码(编译器),并且它们都可以很好地工作。

因此,事实证明,尽管从某种意义上说机器体系结构是必不可少的,但语言可能以非常令人惊讶的方式映射到它们,这看起来不像传统的C风格的流控制,而且如果我们认为它足够低级,这也可能是高效。

最重要的是,还有许多其他优化方法,特别是通过纯度进行了优化,因为它允许更大范围的“安全”转换。何时以及如何应用这些转换,使它们变得更好而不是更糟,这当然是一个经验性的问题,在此以及许多其他小的选择上,多年的工作已投入到理论工作和实践基准中。因此,这当然也起作用。此类研究的一个很好的例子是“ 制作快速咖喱:推/输入与评估/申请高阶语言”。

最后,应该注意的是,由于间接的原因,该模型仍然会带来开销。在我们知道严格执行操作是“安全的”并因此避免了图间接作用的情况下,可以避免这种情况。在GHC Wiki上再次详细记录了推断严格性/需求的机制。


2
需求分析器的链接值得一试!最后,关于该主题的某些内容似乎根本无法解释为不可思议的黑魔法。我怎么没听说过?它应该与任何人问如何懒惰地解决问题的地方联系在一起!
Evi1M4chine

@ Evi1M4chine我没有看到与需求分析器相关的链接,也许它已经以某种方式丢失了。有人可以恢复链接或澄清参考吗?听起来很有趣。
Cris P

1
@CrisP我相信最后一个链接是指什么。它转到GHC Wiki上有关GHC中需求分析器的页面。
蛇纹石美洲狮

@Serpentine Cougar,克里斯·P:是的,这就是我的意思。
Evi1M4chine

19

好吧,这里有很多评论。我会尽量回答。

正确使用它可以接近低级语言。

以我的经验,在很多情况下,通常有可能使Rust的性能提高2倍。但是,在某些(广泛的)用例中,与低级语言相比,性能较差。

甚至击败它,但这意味着您使用的是效率低下的C程序,因为GHC将Haskell编译为C)

这并不完全正确。Haskell编译为C-(C的子集),然后通过本机代码生成器编译为汇编。本机代码生成器通常生成比C编译器更快的代码,因为它可以应用普通C编译器无法执行的某些优化。

机器架构显然必须基于图灵机。

这不是考虑问题的好方法,特别是因为现代处理器会无序地并且可能同时评估指令。

实际上,Haskell甚至没有特定的评估顺序。

实际上,Haskell 确实隐式定义了评估顺序。

另外,无需处理机器数据类型,而是始终创建代数数据类型。

在许多情况下,只要您拥有足够先进的编译器,它们便会对应。

您可能会认为,即时创建函数并将其扔掉会使程序变慢。

Haskell已编译,因此实际上并未即时创建高阶函数。

似乎在优化Haskell代码,您需要使其更优雅抽象,而不是像更多的机器一样。

通常,使代码更像“机器一样”是在Haskell中获得更好性能的无效方法。但是,使其更加抽象也不总是一个好主意。什么一个好主意,用普通的数据结构,并已高度优化的功能(如链表)是。

f x = [x]f = pure在Haskell中完全一样的东西,例如。在前一种情况下,好的编译器不会产生更好的性能。

考虑到Haskell(与GHC一起编译)的抽象性质和与物理机器的区别,为什么这么快?

简短的答案是“因为它被设计为完全做到这一点”。GHC使用无脊椎无标签g机(STG)。您可以在这里阅读有关它的论文(非常复杂)。GHC还做很多其他事情,例如严格性分析和乐观评估

我说C和其他命令式语言在某种程度上类似于Turing Machines的原因(但在某种程度上来说,Haskell与Lambda微积分不一样)是,在命令式语言中,您具有有限数量的状态(即行号),使用磁带(撞锤),从而状态和当前磁带决定对磁带执行的操作。

混淆点会导致代码变慢吗?Haskell的懒惰实际上意味着可变性并不像您想的那么重要,而且它是高级的,因此编译器可以应用许多优化。因此,就地修改记录的速度很少会比使用C等语言慢。


3

为什么Haskell(GHC)这么快?

自从我上次测量Haskell的性能以来,某些事情肯定已经发生了巨大变化。例如:

  • RRD文件处理基准测试中,作者发现Haskell的开发时间更长,并且运行速度比Go(130s)和OCaml(67s)慢(1,020s),即比OCaml慢15倍。
  • 一个简单的字典基准显示Haskell的运行速度比F#慢10倍。
  • 光线跟踪器语言比较是另一个例子,其中的Haskell是比C ++慢,OCaml的甚至Java和Lisp的。
  • 平行通用快速排序在Haskell比F#慢55%,并且需要显着更多的代码。

那么,发生了什么变化?我注意到,这个问题或其当前答案都没有提到任何可验证的基准甚至代码。

Haskellers最喜欢做的事情是尝试使C值控制在5%以内

您是否提到任何人都可以证实的可验证结果?


6
有人再三次在镜子前说出哈罗普的名字吗?
查克·亚当斯

2
不是10倍,但整个条目都是营销炒作和牛肚。就速度而言,GHC确实确实有能力接近C甚至有时克服它,但这通常需要相当复杂的底层编程风格,与C本身的编程没有太大区别。不幸。通常,代码级别越高,速度越慢。空间泄漏,方便但性能不佳的ADT类型(代数的,不是抽象的,如承诺的那样)等)
Will Ness

1
我之所以发布,是因为今天我看到了chrispenner.ca/posts/wc。它是用Haskell编写的wc实用程序的一个实现,据称它胜过了c版本。
驻军

3
@加里森感谢您的链接。我称80行为“低级编程风格,与C本身编程没有太大区别”。。“高级代码”,即为“愚蠢” fmap (length &&& length . words &&& length . lines) readFile。如果是比快(或者甚至可以媲美)C,这里的炒作将被完全有道理然后。关键是,我们仍然需要像在C语言中那样在Haskell中努力提高速度。
尼斯,

2
通过Redditreddit.com/r/programming/comments/dj4if3/…上的讨论,可以判断Haskell代码确实存在错误(例如,以空格开头或结尾的换行符,以à开头的换行符)以及其他代码无法再现所声称的性能结果。
乔恩·哈罗普
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.