为什么Haskell(GHC)如此快速地修补?

Haskell(使用GHC编译器)比你期望的要快得多。 正确使用,它可以与低级语言关系密切。 (Haskellers最喜欢做的事情是尝试在C的5%以内(或者甚至超过C),但这意味着您使用的是低效的C程序,因为GHC将Haskell编译为C)。我的问题是,为什么?

Haskell是声明式的,基于lambda演算。 机器体系结构显然是必不可少的,大致基于图灵机。 的确,Haskell甚至没有具体的评估顺序。 另外,不是处理机器数据类型,而是始终创建代数数据类型。

最奇怪的是高阶函数。 你会认为,即时创建函数并将它们扔掉,会使程序变慢。 但使用更高阶的函数实际上使Haskell更快。 的确,看起来,要优化Haskell代码,您需要使其更加优雅和抽象,而不是像机器一样。 如果Haskell的更高级功能没有改进,它甚至不会影响其性能。

对不起,如果这听起来不错,但这里是我的问题: 为什么Haskell(与GHC编译)如此之快,考虑到它的抽象性质和与物理机器的差异?

注意:我说C语言和其他命令式语言的原因有点类似于图灵机(但并不像Haskell类似于Lambda微积分),在命令式语言中,你的状态数量有限(又称行号) ,以及一个磁带(RAM),以便状态和当前磁带决定如何处理磁带。 查看维基百科条目,图灵机等价物,用于从图灵机到计算机的转换。


我认为这是一个有点意见的基础。 但我会试着回答。

我同意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) ,那么这是硬编码到你的源代码中。 它在运行时不能改变。 所以这不是一个真正的动态功能。 即使是curried函数也只是将参数保存到数据块中。 所有的可执行代码实际上都在编译时存在; 没有运行时解释。 (与其他一些具有“eval函数”的语言不同)。

想想帕斯卡。 它已经很老了,没有人再使用它了,但没有人会抱怨Pascal很慢。 有很多事情不喜欢它,但慢的并不是其中之一。 Haskell实际上并没有那么做,这与Pascal不同,除了垃圾回收而不是手动内存管理。 不变数据允许对GC引擎进行若干优化[懒惰评估稍微复杂化]。

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

真正的杀手是懒惰评估。 当你的代码严格/懒惰的时候,你可以编写出高雅而又美丽的代码。 但是,如果你弄错了这个东西,你的程序会慢几千倍,而且为什么会发生这种情况真的不明显。

例如,我写了一个简单的小程序来计算每个字节在文件中出现的次数。 对于25KB的输入文件,程序需要20分钟才能运行并吞下6千兆字节的RAM! 这太荒唐了! 但后来我意识到问题所在,并添加了一个单一的模式,运行时间缩短到0.02秒

这是Haskell意外缓慢下降的地方。 它肯定需要一段时间才能习惯它。 但随着时间的推移,编写真正快速的代码变得更容易。

是什么让Haskell如此之快? 纯度。 静态类型。 懒惰。 但最重要的是,编译器可以在不破坏代码期望的情况下从根本上改变实现,从而达到足够高的水平。

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


很长时间以来,人们认为功能语言不可能很快 - 特别是懒惰的功能语言。 但是这是因为他们早期的实现本质上是解释的而不是真正的编译。

第二波设计出现在图缩减的基础上,并开辟了更高效编译的可能性。 Simon Peyton Jones在他的两本着作“函数式编程语言的实现和实现函数式语言”中写到了这项研究:一篇教程(前者由Wadler和Hancock编写,后者由David Lester编写)。 (Lennart Augustsson也告诉我,前一本书的一个关键动机是描述了他的LML编译器没有广泛评论的方式完成了它的编译)。

在这些作品中描述的图缩减方法背后的关键概念是,我们不把程序看作是一系列指令,而是将一个依赖图通过一系列局部缩减进行评估。 第二个关键洞察是对这样的图的评估不需要被解释,而是图的本身可以由代码构建。 特别是,我们可以表示一个图的节点,而不是“值或操作码”和“操作的值”,而是作为调用时的函数返回所需的值。 第一次被调用时,它会向子节点询问它们的值,然后对它们进行操作,然后用一条只说“返回结果”的新指令覆盖自身。

这是在奠定了基础了如何GHC今天仍然适用(尽管许多模各种调整),后面的论文中描述:“在股票硬件实现懒功能语言:该骨气无需生成代码G-机”。 GHC Wiki的当前执行模式更详细地记录在案。

因此,我们的见解是,严格区分“数据”和“代码”,我们认为这对机器如何工作“基本”并不是它们必须如何工作,而是由我们的编译器施加的。 所以我们可以抛出这个问题,并拥有生成自修改代码(可执行文件)的代码(一个编译器),它可以很好地工作。

因此,事实证明,虽然机器架构在某种意义上是必不可少的,但语言可能会以非常令人惊讶的方式映射到它们,看起来不像传统的C型流控制,并且如果我们认为低级别,这也可能是高效。

除此之外,还有许多其他的优化,特别是纯度开放,因为它允许更大范围的“安全”转换。 何时以及如何应用这些转变,以便让事情变得更好而不是更糟,当然是一个经验问题,在这个和其他许多小的选择上,多年的工作已经投入理论工作和实践基准。 所以这当然也起到一定的作用。 一篇提供此类研究的很好例子的文章是“快速制作咖喱:推/输入与评估/申请高级语言”。

最后,应该指出的是,这种模式仍然由于间接导致了开销。 如果我们知道严格执行某些操作是“安全的”,并因此避免了图形间接的情况,则可以避免这种情况。 在GHC Wiki中再次详细记录了推断严格性/需求的机制。


那么,这里有很多评论。 我会尽力回答。

正确使用,它可以与低级语言关系密切。

根据我的经验,在很多情况下,通常可以获得2倍以上的锈效果。 但是也有一些(广泛的)用例,其性能比低级语言差。

或者甚至击败它,但这意味着你正在使用低效的C程序,因为GHC将Haskell编译为C)

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

机器体系结构显然是必不可少的,大致基于图灵机。

这不是一个好的思考方式,特别是因为现代处理器会评估指令并可能在同一时间。

的确,Haskell甚至没有具体的评估顺序。

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

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

如果你有足够先进的编译器,它们在很多情况下都是相对应的。

你会认为,即时创建函数并将它们扔掉,会使程序变慢。

Haskell被编译,因此高阶函数实际上并不是实时创建的。

它似乎优化了Haskell代码,你需要使它更优雅和抽象,而不是像更多的机器。

一般来说,使代码更“机器化”是在Haskell中获得更好性能的非生产性方式。 但是使它更抽象并不总是一个好主意。 最好的办法是使用经过大量优化的通用数据结构和函数(如链表)。

比如, fx = [x]f = pure在Haskell中完全相同。 一个好的编译器在前一种情况下不会产生更好的性能。

为什么Haskell(与GHC一起编译)如此之快,考虑到它的抽象性质和与物理机器的差异?

简短的回答是“因为它的目的就是为了做到这一点。” GHC使用无骨刺无标签g-machine(STG)。 你可以在这里阅读关于它的论文(这非常复杂)。 GHC也做了很多其他事情,比如严格分析和乐观评估。

我说C语言和其他命令式语言的原因有点类似于图灵机(但在Haskell与Lambda微积分相似的程度上)是因为在命令式语言中,你有一定数量的状态(又称行号)用磁带(ram),以使状态和当前磁带决定对磁带做什么。

难以理解,可变性会导致代码变慢吗? Haskell的懒惰实际上意味着可变性不像你想象的那么重要,再加上它是高层次的,所以编译器可以应用很多优化。 因此,就地修改记录的速度通常比在诸如C这样的语言中更慢。

链接地址: http://www.djcxy.com/p/5681.html

上一篇: Why is Haskell (GHC) so darn fast?

下一篇: Defining a new monad in haskell raises no instance for Applicative