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

PyR*_*lez 223 performance haskell lambda-calculus ghc higher-order-functions

Haskell(使用GHC编译器)比你期望的要快很多.如果使用得当,它可以与低级语言保持密切联系.(Haskellers最喜欢的事情是尝试在C的5%范围内(或者甚至击败它,但这意味着你使用的是低效的C程序,因为GHC将Haskell编译为C).)我的问题是,为什么?

Haskell是声明性的,基于lambda演算.机器架构显然势在必行,粗略地基于图灵机.实际上,Haskell甚至没有具体的评估顺序.此外,您不必处理机器数据类型,而是始终生成代数数据类型.

最奇怪的是更高阶函数.你会认为动态创建函数并抛出它们会使程序变慢.但是使用更高阶函数实际上使Haskell更快.实际上,似乎要优化Haskell代码,你需要使它更优雅和抽象,而不是更像机器.如果Haskell的更高级功能没有改进,它们似乎甚至都不会影响它的性能.

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

注意:我说C和其他命令式语言有点类似于图灵机的原因(但不是Haskell类似于Lambda微积分)是因为在命令式语言中,你有一个有限数量的状态(也就是行号) ,以及磁带(ram),以便状态和当前磁带确定对磁带做什么.有关从图灵机到计算机的过渡,请参阅维基百科条目,图灵机等效项.

Mat*_*hid 239

我认为这个是基于意见的.但我会尽力回答.

我同意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函数实际上只是将参数保存到数据块中.所有可执行代码实际上都在编译时存在; 没有运行时解释.(与其他一些具有"评估功能"的语言不同.)

想想Pascal.它已经老了,没有人真正使用它,但是没有人会抱怨Pascal很.有很多东西不喜欢它,但缓慢并不是真正的其中之一.除了垃圾收集而不是手动内存管理之外,Haskell并没有真正做到与Pascal不同的那么多.并且不可变数据允许对GC引擎进行多次优化[懒惰评估然后稍微复杂化].

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

真正的杀手是懒惰的评价.当你正确地得到你的代码的严格/懒惰时,你可以写出仍然优雅和美丽的愚蠢的快速代码.但如果你把这些东西弄错了,你的程序会慢几千倍,而且为什么会发生这种情况真的不明显.

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

就是Haskell出乎意料地慢慢走的地方.确实需要一段时间才能适应它.但随着时间的推移,编写真正快速的代码会变得更容易.

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

但我猜这只是我的意见......

  • "比方说,Java,JVM必须检查空指针,如果你尊重它,就会抛出异常." Java的隐式空检查(大部分)是无成本的.Java实现能够而且确实采取虚拟内存的优势,以空地址映射到一个缺页,因此取消对NULL指针触发在CPU级别的页面故障,哪个Java捕获并抛出一个高层次的异常.因此,大多数空值检查由CPU中的内存映射单元完成,免费. (30认同)
  • @cimmanon我不认为这纯粹是基于意见的.这是一个有趣的问题,其他人可能想要答案.但我想我们会看到其他选民的想法. (12认同)
  • @MathematicalOrchid:你有原始程序的副本需要20分钟才能运行吗?我认为研究为什么它如此缓慢是非常有益的. (10认同)
  • @cimmanon - 该搜索仅提供一个半线程,并且它们都与审核审核有关.并且对该主题的赞成回答说:"请停止管理你不理解的东西." 我建议,如果有人认为对此的答案必然过于广泛,那么他们会感到惊讶并享受答案,因为答案不是太宽泛. (7认同)
  • @cimmanon:也许这是因为Haskell用户似乎是唯一一个真正友善的群体......你认为这是一个"玩笑"......而不是一个统治纳粹的狗吃狗社区他们一有机会就互相撕掉一个新的...这似乎是你认为的"正常". (4认同)
  • @WillNess——这是一个陷阱22。我声称这个问题_不太_太宽泛,因为它本身就可以得到一个相对简洁的答案。但你声称它_无法_通过相对简洁的答案来回答。我会回答,仅仅因为你无法想象这样的答案并不意味着这个答案不存在。这只是意味着您可能会从中学到一些东西。我想这就是堆栈溢出的重点。 (3认同)
  • 我用C++编写了一个基于哈希表的bytecounter版本,共有7行,其中两行是不必要的,它在8.1ms内分析了28KB的文件.我也很好奇你如何用高级语言管理20分钟以迎合激进的优化,为什么即使正确地敲击它,它也不具备现代低级语言的竞争力. (3认同)
  • 我确实记得过去人们普遍指责帕斯卡很慢。然而,它是基于早期的 Pascal 实现,运行“P 代码”(一种字节码中介),这实际上使其更接近于 JIT 之前的 java,而在 80 年代初期,速度实在是太慢了。然而,Turbo Pascal 和许多非学术界编译器提供了真正诚实的机器代码编译器,并且使 Turbo Pascal 具有令人眼花缭乱的速度,与 Turbo C/C++(与之共享后端)相当。每个人都认为 Borland C 速度很快。但 Borland Pascal 也同样快。相同的编译器! (3认同)
  • 您确实了解到,Haskell问题是有关在此处审核问题的每个笑话的对头,对吗?Haskellers会欢迎过分广泛或自以为是的问题,只要这些问题在其他所有标签中都可以正确地解决。最重要的是,这个问题得到了Reddit(即什至没有参加该社区的人)的大力支持。 (2认同)
  • @Boann 这很有趣。我不知道。 (2认同)
  • @MathematicalOrchid,如果您详细说明字节计数器示例(在注释中说),那就太好了。我很好奇是什么让你受到了如此大的惩罚(6GB,20 分钟!) (2认同)
  • “Haskell 非常高级[可以让编译器很好地优化]”。您能否定义高级性并解释它如何使编译器能够更好地优化?schema、python 和 javascript 的高级程度一样吗?它们同样可优化吗?如果没有,为什么不呢?我的看法是:程序承认的执行策略越多,执行策略就越快(并且编译器人员会找到一种快速的策略)。纯代码(Haskell 和只读 SQL 查询)允许许多策略。必须排序的副作用会缩小这个空间,因此更难优化。 (2认同)
  • @George 我还没有这个程序。回顾起来,我认为真正的杀手是程序的工作集是 6GB,而机器只有 128MB 的物理 RAM。在任何编程语言中这都会很慢......(还应该告诉你这一切发生了多久!) (2认同)

scl*_*clv 70

很长一段时间以来,人们一直认为函数式语言不是很快 - 特别是懒惰的函数式语言.但这是因为他们的早期实现本质上是解释而不是真正编译.

第二波设计基于图形缩减而出现,并为更高效的编译开辟了可能性.Simon Peyton Jones在他的两本书"功能编程语言的实现实现函数式语言:一个教程(前者有Wadler和Hancock的部分,后者用David Lester编写)"中写到了这项研究.(Lennart Augustsson还告诉我,前一本书的一个主要动机是描述他的LML编译器的方式,该编译器没有得到广泛的评论,完成了它的编译).

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

这在后面的文章中进行了描述,该文章介绍了GHC如何在今天仍然有效的基础(虽然模数很多种调整):"在库存硬件上实现懒惰功能语言:无脊椎无标签G-Machine"..GHC的当前执行模型在GHC Wiki中有更详细的记录.

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

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

除此之外,还有许多其他优化由纯度打开,因为它允许更大范围的"安全"转换.何时以及如何应用这些转变使得它们使事情变得更好而不是更糟,当然是一个经验问题,在这个和许多其他小的选择上,多年的工作已经被纳入理论工作和实际基准.所以这当然也起了作用.提供此类研究的一个很好的例子的论文是" 快速咖喱:推/输与对比/适用于高阶语言".

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

  • @CrisP 我相信最后一个链接是所指的内容。它转到 GHC Wiki 上有关 GHC 中的需求分析器的页面。 (3认同)
  • 需求分析器链接值得您一试!最后,关于该主题的某些内容似乎根本无法解释为不可思议的黑魔法。我怎么没听说过?它应该与任何人都问如何懒惰地解决问题的地方联系在一起! (2认同)

小智 17

嗯,这里有很多评论.我会尽可能多地回答.

如果使用得当,它可以与低级语言保持密切联系.

根据我的经验,在很多情况下,通常可以将Rust的性能提高2倍.但是也存在一些(广泛的)用例,与低级语言相比,性能较差.

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

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

机器架构显然势在必行,粗略地基于图灵机.

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

实际上,Haskell甚至没有具体的评估顺序.

其实,哈斯克尔没有隐含地定义的评估顺序.

此外,您不必处理机器数据类型,而是始终生成代数数据类型.

如果您拥有足够先进的编译器,它们在许多情况下都是对应的.

你会认为动态创建函数并抛出它们会使程序变慢.

Haskell是编译的,因此高阶函数实际上并不是动态创建的.

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

通常,使代码更"机器化"是在Haskell中获得更好性能的非生产性方法.但是让它更抽象也不总是一个好主意.什么好主意是使用经过大量优化的常见数据结构和函数(例如链表).

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

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

简短的回答是"因为它的设计就是为了做到这一点." GHC使用无脊椎无标记g-machine(STG).你可以在这里阅读一篇关于它的论文(它非常复杂).GHC还做了很多其他事情,例如严格性分析和乐观评估.

我说C和其他命令式语言有点类似于图灵机的原因(但不是Haskell类似于Lambda微积分)是因为在命令式语言中,你有一个有限数量的状态(也就是行号),使用磁带(ram),以便状态和当前磁带确定如何对磁带执行操作.

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