关于性能:为什么Haskell(GHC)如此快速?

Why is Haskell (GHC) so darn fast?

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

Haskell是声明性的,基于lambda微积分。机器架构显然是必要的,大致上基于图灵机器。实际上,Haskell甚至没有特定的评估顺序。此外,您不必处理机器数据类型,而是始终生成代数数据类型。

最奇怪的是高阶函数。您会认为,在运行中创建函数,并将它们抛在一边,会使程序变慢。但是使用高阶函数实际上会使haskell更快。实际上,为了优化haskell代码,您需要使它更优雅和抽象,而不是更像机器。如果没有改进的话,哈斯克尔更高级的特性似乎都不会影响它的性能。

抱歉,如果这听起来很粗俗,但我的问题是:为什么Haskell(用ghc编译)这么快,考虑到它的抽象性质和与物理机器的区别?

注:我之所以说C语言和其他命令式语言与图灵机器有些相似(但哈斯克尔与lambda演算不太相似),是因为在命令式语言中,有有限数量的状态(a.k.a.行号),以及一个磁带(RAM),这样状态和当前磁带就决定了要做什么。录音带。有关从图灵机器到计算机的转换,请参阅维基百科中的图灵机器等效项。


我觉得这个有点基于观点。但我会尽力回答。好的。

我同意Dietrich EPP的观点:它是多种因素的结合,使GHC快速发展。好的。

首先,也是最重要的一点,哈斯克尔是非常高层次的。这使编译器能够在不破坏代码的情况下执行积极的优化。好的。

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

我认为哈斯克尔是一样的。您可能认为您只是要求haskell将输入列表映射到第二个列表,将第二个列表筛选到第三个列表,然后计算得到的项目数。但是,您没有看到ghc在幕后应用流融合重写规则,将整个过程转换为一个严格的机器代码循环,在不分配数据的情况下以一次传递的方式完成整个工作—这类事情将是乏味的、容易出错的,并且手工编写是不可维护的。这仅仅是因为代码中缺少低级的细节。好的。

另一种看问题的方法可能是…为什么haskell不应该快点?它做了什么会使它变慢?好的。

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

与OO语言不同的是[Java]、CysIcript、JavaScript和Helip;] Haskell具有完全类型擦除(例如C、C++、Pascal和Helip;)。所有类型检查只在编译时进行。所以也没有运行时类型检查来减慢你的速度。(没有空指针检查,就这点而言。在Java中,JVM必须检查空指针并抛出异常。哈斯克尔不必为那张支票费心。)好的。

你说"在运行时动态地创建函数"听起来很慢,但是如果你仔细看的话,实际上你不会这么做。它可能看起来像你做的,但你不做。如果你说(+5),那么,这是硬编码到你的源代码中的。它不能在运行时更改。所以它不是一个动态函数。即使是循环函数也只是将参数保存到数据块中。所有可执行代码实际上都存在于编译时;没有运行时解释。(与其他具有"eval函数"的语言不同。)好的。

想想帕斯卡。它很旧,没有人真的用它了,但没有人会抱怨帕斯卡慢。有很多事情是不喜欢的,但缓慢并不是其中之一。haskell和pascal没有太大的区别,除了进行垃圾收集,而不是手动内存管理。不可变的数据允许对GC引擎进行一些优化[这使得延迟的评估变得有些复杂]。好的。

我认为哈斯克尔看起来先进、老练、高水平,每个人都认为"哦,哇,这真的很强大,一定是太慢了!"但事实并非如此,或者至少,这并不是你所期望的那样。是的,它有一个惊人的类型系统。但你知道吗?这一切都发生在编译时。到了运行时间,它就不见了。是的,它允许您用一行代码构造复杂的ADT。但你知道吗?ADT只是一个普通的unionstructS.没有更多。好的。

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

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

这就是哈斯凯尔出人意料地缓慢前进的地方。当然要花一段时间才能习惯。但随着时间的推移,编写真正快速的代码变得更加容易。好的。

是什么让哈斯克尔这么快?纯度。静态类型。懒惰。但最重要的是,它具有足够高的级别,编译器可以在不违背代码期望的情况下彻底更改实现。好的。

但我想这只是我的意见。好的。好啊。


很长一段时间以来,人们认为函数式语言不能很快——尤其是懒惰的函数式语言。但这是因为它们的早期实现本质上是解释的,而不是真正编译的。

第二批基于图形简化的设计出现了,为更高效的编译提供了可能。西蒙·佩顿·琼斯在他的两本书《函数式编程语言的实现和函数式语言的实现:教程》(前者有韦德勒和汉考克的章节,后者有大卫·莱斯特的作品)中写到了这项研究。(LennartAugutsson还告诉我,前一本书的一个关键动机是描述他的LML编译器(没有被广泛评论)完成编译的方式。

这些著作中描述的图形简化方法背后的关键概念是,我们不将程序看作指令序列,而是通过一系列局部简化来评估依赖关系图。第二个关键的洞察是,这种图的评估不需要解释,而是可以用代码构建图本身。特别是,我们可以表示一个图的节点,而不是"值或‘操作码’和要操作的值",而是作为一个函数,当调用时,返回所需的值。第一次调用它时,它向子节点请求它们的值,然后对它们进行操作,然后它用一条新的指令覆盖自己,该指令只说"返回结果"。

这在后面的一篇文章中进行了描述,该文章阐述了目前GHC仍然工作的基本原理(尽管模块化了许多不同的调整):"在库存硬件上实现懒惰的功能语言:无精打采的无标签G机"。GHC的当前执行模型在GHC wiki中有更详细的记录。

因此,我们的洞见是,我们认为机器如何工作的"数据"和"代码"的严格区别不是它们必须如何工作,而是由我们的编译器强加的。所以我们可以抛弃它,让代码(编译器)生成自我修改的代码(可执行文件),它可以很好地工作。

因此,事实证明,虽然机器架构在某种意义上是必要的,但是语言可能以非常令人惊讶的方式映射到它们,而不像传统的C风格流控制,如果我们认为级别足够低,这也可能是有效的。

除此之外,还有许多其他的优化,特别是由Purity开发的,因为它允许更大范围的"安全"转换。当然,何时以及如何应用这些转变使事情变得更好而不是更糟是一个经验问题,在这一点上以及许多其他的小选择上,多年的工作已经投入到理论工作和实践基准中。所以这当然也起到了作用。这类研究的一个很好的例子是"制造一种快速的咖喱:推送/输入vs.eval/应用于高阶语言"。

最后,应该注意的是,这个模型仍然引入了间接导致的开销。在我们知道严格地做事情是"安全"的情况下,可以避免这种情况,从而消除图形间接性。推断严格性/需求的机制再次在GHC wiki中详细记录。


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

Used correctly, it can get close-ish to low-level languages.

根据我的经验,在许多情况下,通常可以达到防锈性能的2倍。但是也有一些(广泛的)用例的性能比低级语言差。

or even beat it, but that means you are using an inefficient C program, since GHC compiles Haskell to C)

这并不完全正确。haskell编译成C——(C的一个子集),然后通过本机代码生成器编译成程序集。本机代码生成器通常比C编译器生成的代码更快,因为它可以应用一些普通C编译器无法应用的优化。

Machine architectures are clearly imperative, being based on turing machines, roughly.

这不是一个好的思考方法,特别是因为现代处理器将不按顺序评估指令,而且可能同时评估指令。

Indeed, Haskell doesn't even have a specific evaluation order.

实际上,haskell确实隐式地定义了一个计算顺序。

Also, instead of dealing with machine data types, you make algebraic data types all the time.

如果您有足够高级的编译器,它们在许多情况下都是对应的。

You would think that creating functions on the fly, and throwing them around, would make a program slower.

haskell是编译的,因此高阶函数实际上不是即时创建的。

it seems to optimize Haskell code, you need to make it more elegant and abstract, instead of more machine like.

一般来说,让代码更"像机器"是在Haskell中获得更好性能的一种非生产性方法。但让它更抽象也不总是一个好主意。一个好主意是使用已经过大量优化的公共数据结构和函数(如链表)。

例如,在哈斯克尔,f x = [x]f = pure是完全相同的。在前一种情况下,一个好的编译器不会产生更好的性能。

Why is Haskell (compiled with GHC) so fast, considering its abstract nature and differences from physical machines?

简短的回答是"因为它的设计就是为了做到这一点",GHC使用了无脊椎无标签G机(STG)。你可以在这里读到一篇有关它的论文(它很复杂)。GHC也做了很多其他的事情,比如严格的分析和乐观的评价。

The reason I say C and other imperative languages are somewhat similar to Turing Machines (but not to the extent that Haskell is similar to Lambda Calculus) is that in an imperative language, you have a finite number of states (a.k.a. line number), along with a Tape (the ram), such that the state and the current tape determine what to do to the tape.

那么,混淆的重点是易变性会导致代码变慢吗?haskell的懒惰实际上意味着易变性没有你想象的那么重要,加上它是高级的,所以编译器可以应用许多优化。因此,在适当位置修改记录的速度很少比在C等语言中慢。