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

Haskell是声明性的,并且基于lambda演算。机器架构显然是必要的,大致上基于图灵机。事实上,Haskell甚至没有一个具体的评估顺序。此外,与处理机器数据类型不同,您一直都在使用代数数据类型。

最奇怪的是高阶函数。您可能会认为,动态地创建函数并将它们到处扔,会使程序变慢。但是使用高阶函数实际上使Haskell更快。实际上,为了优化Haskell代码,你需要让它更优雅和抽象,而不是更像机器。Haskell的任何高级特性如果不加以改进,似乎都不会影响它的性能。

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

注意:我之所以说C和其他命令式语言有点类似于图灵机(但没有到Haskell类似于Lambda Calculus的程度),是因为在命令式语言中,您有有限数量的状态(也就是行号),以及一个磁带(ram),这样状态和当前磁带就决定了对磁带做什么。关于从图灵机到计算机的转变,请参阅维基百科的条目,图灵机等价物。


当前回答

我同意Dietrich Epp的观点:GHC的快速是由几个因素共同作用的结果。

首先,Haskell的级别很高。这使得编译器可以在不破坏代码的情况下执行积极的优化。

Think about SQL. Now, when I write a SELECT statement, it might look like an imperative loop, but it isn't. It might look like it loops over all rows in that table trying to find the one that matches the specified conditions, but actually the "compiler" (the DB engine) could be doing an index lookup instead — which has completely different performance characteristics. But because SQL is so high-level, the "compiler" can substitute totally different algorithms, apply multiple processors or I/O channels or entire servers transparently, and more.

I think of Haskell as being the same. You might think you just asked Haskell to map the input list to a second list, filter the second list into a third list, and then count how many items resulted. But you didn't see GHC apply stream-fusion rewrite rules behind the scenes, transforming the entire thing into a single tight machine code loop that does the whole job in a single pass over the data with no allocation — the kind of thing that would be tedious, error-prone and non-maintainable to write by hand. That's only really possible because of the lack of low-level details in the code.

从另一个角度来看,Haskell为什么不能快呢?它做了什么会让它变慢?

它不是像Perl或JavaScript那样的解释性语言。它甚至不是像Java或c#那样的虚拟机系统。它一直编译到本地机器代码,所以没有开销。

与面向对象语言(Java、c#、JavaScript等)不同,Haskell具有完全类型擦除功能(如C、c++、Pascal等)。所有类型检查只在编译时发生。因此,也没有运行时类型检查来降低您的速度。(就此而言,没有空指针检查。例如,在Java中,JVM必须检查空指针,如果遵从一个空指针,则抛出异常。Haskell不需要为支票而烦恼。)

You say it sounds slow to "create functions on the fly at run-time", but if you look very carefully, you don't actually do that. It might look like you do, but you don't. If you say (+5), well, that's hard-coded into your source code. It cannot change at run-time. So it's not really a dynamic function. Even curried functions are really just saving parameters into a data block. All the executable code actually exists at compile-time; there is no run-time interpretation. (Unlike some other languages that have an "eval function".)

想想帕斯卡。它很旧了,没有人再用它了,但是没有人会抱怨帕斯卡很慢。它有很多令人不喜欢的地方,但慢并不是其中之一。Haskell除了垃圾收集而不是手动内存管理之外,并没有做太多与Pascal不同的事情。不可变的数据允许对GC引擎进行一些优化(惰性计算会使其变得有些复杂)。

我认为Haskell看起来很先进,很复杂,很高级,每个人都觉得“哦,哇,这真的很强大,它一定非常慢!”但事实并非如此。或者至少,它不是你所期望的方式。是的,它有一个惊人的类型系统。但你知道吗?这一切都发生在编译时。到运行时,它就消失了。是的,它允许你用一行代码构建复杂的adt。但你知道吗?ADT只是一个普通的C结构并集。仅此而已。

真正的杀手是懒惰的评估。当你正确地处理代码的严格/懒惰时,你可以写出非常快的代码,但仍然优雅而美丽。但是如果你做错了,你的程序就会变慢几千倍,为什么会发生这样的事情真的不太明显。

例如,我写了一个简单的小程序来计算每个字节在文件中出现的次数。对于一个25KB的输入文件,该程序运行了20分钟,占用了6gb的RAM!这是荒谬的! !但后来我意识到问题所在,添加了一个刘海图案,运行时间下降到0.02秒。

哈斯克尔在这里走得出乎意料地慢。这当然需要一段时间来适应。但是随着时间的推移,编写非常快速的代码变得更加容易。

是什么让哈斯克尔这么快?纯洁。静态类型。懒惰。但最重要的是,要足够高级,编译器可以在不破坏代码预期的情况下从根本上改变实现。

但我想这只是我的个人观点……

其他回答

我同意Dietrich Epp的观点:GHC的快速是由几个因素共同作用的结果。

首先,Haskell的级别很高。这使得编译器可以在不破坏代码的情况下执行积极的优化。

Think about SQL. Now, when I write a SELECT statement, it might look like an imperative loop, but it isn't. It might look like it loops over all rows in that table trying to find the one that matches the specified conditions, but actually the "compiler" (the DB engine) could be doing an index lookup instead — which has completely different performance characteristics. But because SQL is so high-level, the "compiler" can substitute totally different algorithms, apply multiple processors or I/O channels or entire servers transparently, and more.

I think of Haskell as being the same. You might think you just asked Haskell to map the input list to a second list, filter the second list into a third list, and then count how many items resulted. But you didn't see GHC apply stream-fusion rewrite rules behind the scenes, transforming the entire thing into a single tight machine code loop that does the whole job in a single pass over the data with no allocation — the kind of thing that would be tedious, error-prone and non-maintainable to write by hand. That's only really possible because of the lack of low-level details in the code.

从另一个角度来看,Haskell为什么不能快呢?它做了什么会让它变慢?

它不是像Perl或JavaScript那样的解释性语言。它甚至不是像Java或c#那样的虚拟机系统。它一直编译到本地机器代码,所以没有开销。

与面向对象语言(Java、c#、JavaScript等)不同,Haskell具有完全类型擦除功能(如C、c++、Pascal等)。所有类型检查只在编译时发生。因此,也没有运行时类型检查来降低您的速度。(就此而言,没有空指针检查。例如,在Java中,JVM必须检查空指针,如果遵从一个空指针,则抛出异常。Haskell不需要为支票而烦恼。)

You say it sounds slow to "create functions on the fly at run-time", but if you look very carefully, you don't actually do that. It might look like you do, but you don't. If you say (+5), well, that's hard-coded into your source code. It cannot change at run-time. So it's not really a dynamic function. Even curried functions are really just saving parameters into a data block. All the executable code actually exists at compile-time; there is no run-time interpretation. (Unlike some other languages that have an "eval function".)

想想帕斯卡。它很旧了,没有人再用它了,但是没有人会抱怨帕斯卡很慢。它有很多令人不喜欢的地方,但慢并不是其中之一。Haskell除了垃圾收集而不是手动内存管理之外,并没有做太多与Pascal不同的事情。不可变的数据允许对GC引擎进行一些优化(惰性计算会使其变得有些复杂)。

我认为Haskell看起来很先进,很复杂,很高级,每个人都觉得“哦,哇,这真的很强大,它一定非常慢!”但事实并非如此。或者至少,它不是你所期望的方式。是的,它有一个惊人的类型系统。但你知道吗?这一切都发生在编译时。到运行时,它就消失了。是的,它允许你用一行代码构建复杂的adt。但你知道吗?ADT只是一个普通的C结构并集。仅此而已。

真正的杀手是懒惰的评估。当你正确地处理代码的严格/懒惰时,你可以写出非常快的代码,但仍然优雅而美丽。但是如果你做错了,你的程序就会变慢几千倍,为什么会发生这样的事情真的不太明显。

例如,我写了一个简单的小程序来计算每个字节在文件中出现的次数。对于一个25KB的输入文件,该程序运行了20分钟,占用了6gb的RAM!这是荒谬的! !但后来我意识到问题所在,添加了一个刘海图案,运行时间下降到0.02秒。

哈斯克尔在这里走得出乎意料地慢。这当然需要一段时间来适应。但是随着时间的推移,编写非常快速的代码变得更加容易。

是什么让哈斯克尔这么快?纯洁。静态类型。懒惰。但最重要的是,要足够高级,编译器可以在不破坏代码预期的情况下从根本上改变实现。

但我想这只是我的个人观点……

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

A second wave of designs emerged based on graph reduction, and opened up the possibility for much more efficient compilation. Simon Peyton Jones wrote about this research in his two books The Implementation of Functional Programming Languages and Implementing functional languages: a tutorial (the former with sections by Wadler and Hancock, and the latter written with David Lester). (Lennart Augustsson also informed me that one key motivation for the former book was describing the way that his LML compiler, which was not extensively commented, accomplished its compilation).

The key notion behind graph reduction approaches such as described in these works is that we do not think of a program as a sequence of instructions, but of a dependency graph which is evaluated through a series of local reductions. The second key insight is that the evaluation of such a graph need not be interpreted but instead the graph itself can be built of code. In particular, we can represent a node of a graph not as "either a value or an 'opcode' and the values to operate on" but instead as a function that when invoked, returns the desired value. The first time it is invoked, it asks the subnodes for their values and then operates on them, and then it overwrites itself with a new instruction that just says "return the result."

这在后来的一篇论文中描述了GHC如何在今天仍然工作的基础(尽管进行了许多不同的调整):“在库存硬件上实现惰性函数语言:无标签的G-Machine”。GHC的当前执行模型在GHC Wiki中有更详细的文档。

因此,我们的见解是,“数据”和“代码”之间的严格区别,我们认为这是机器工作的“基础”,但不是它们必须这样工作,而是由我们的编译器强加的。因此,我们可以抛弃它,让代码(编译器)生成自我修改的代码(可执行文件),这一切都可以很好地工作。

因此,虽然机器架构在某种意义上是必要的,但语言可能会以非常令人惊讶的方式映射到它们,这些方式看起来不像传统的c风格的流控制,如果我们足够低级,这也可能是有效的。

On top of this there are many other optimizations opened up by purity in particular, as it allows a greater range of "safe" transformations. When and how to apply these transformations such that they make things better and not worse is of course an empirical question, and on this and many other small choices, years of work have been put into both theoretical work and practical benchmarking. So this of course plays a role as well. A paper that provides a good example of this sort of research is "Making a Fast Curry: Push/Enter vs. Eval/Apply for Higher-Order Languages."

最后,应该注意的是,由于间接,该模型仍然引入了开销。这可以避免在我们知道严格地做事情是“安全的”的情况下,从而省略图的间接。GHC Wiki中再次详细记录了推断严格/要求的机制。

这里有很多值得评论的地方。我会尽量多回答的。

如果使用得当,它可以接近低级语言。

根据我的经验,在很多情况下,它的性能通常可以达到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-machine (STG)。你可以在这里阅读一篇关于它的论文(它相当复杂)。GHC还做了很多其他的事情,比如严格性分析和乐观评估。

我之所以说C和其他命令式语言有点类似于图灵机(但没有到Haskell类似于Lambda Calculus的程度),是因为在命令式语言中,您有有限数量的状态(也就是行号),以及一个磁带(ram),这样状态和当前磁带就决定了对磁带做什么。

那么让人困惑的是可变性会导致代码变慢吗?Haskell的惰性实际上意味着可变性并不像你想象的那么重要,加上它是高级的,所以编译器可以应用许多优化。因此,就地修改记录几乎不会比在C等语言中慢。