总的来说,一个函数式语言的编译器能否进行一些无法在命令式语言中实现的优化,即使使用了尽可能多的不可变性呢?
我是否正确地认为,F#编译器可以通过处理大部分不可变代码来进行某些优化?
很遗憾,不是这样的。对于编译器编写者而言,“大部分不可变”和“不可变”之间存在巨大的差异。即使是保证不可变性也并不那么重要,主要的好处是可以编写非常激进的内联函数。
通常情况下,函数式语言的编译器能够进行哪些无法在命令式语言中实现的优化呢——即使是尽可能使用不可变性编写的命令式语言?
是的,但主要是能够更容易地在更多的地方应用经典的优化技术。例如,不可变性使得应用公共子表达式消除变得更加容易,因为不可变性可以保证您不会更改某些内存单元的内容。
另一方面,如果你的函数式语言不仅是不可变的,而且是纯的(没有像I/O这样的副作用),那么你就可以启用一类涉及将源级表达式重写为更高效表达式的优化。其中最重要且最有趣的是短路砍树,这是一种避免为中间结果分配内存空间的方法。一个很好的例子是流融合。
如果你正在编译一个静态类型的函数式语言以实现高性能,以下是一些重点:
有效利用内存。尽可能使用“非装箱”值,避免分配和额外的间接引用到堆上。特别是流融合和其他消除分配技术非常有效,因为它们消除了分配。
拥有超快速的分配器,并将堆耗尽检查分摊到多个分配中。
有效地内联函数。特别是在模块边界内联小函数。
有效地表示一级函数,通常通过闭包转换实现。高效处理部分应用函数。
不要忽视经典的标量和循环优化。它们对像TIL和Objective Caml这样的编译器产生了巨大的影响。
如果你使用像Haskell或Clean这样的惰性函数语言,还有很多专门处理thunks的事情。
脚注:
完全不可变性带来的一个有趣选项是更能执行非常细粒度的并行。这个故事的结局还没有被告知。
为F#编写一个好的编译器比编写典型的编译器(如果有这样的东西)更难,因为F#受到严格限制:它必须很好地做函数式的事情,但它也必须在.NET框架内有效地工作,而该框架并不是为函数式语言而设计的。我们要感谢Don Syme及其团队在一个受到严格限制的问题上做出了如此出色的工作。
编号。
F#编译器不会尝试分析方法或lambda的引用透明性。.NET BCL根本没有为此设计。
F#语言规范确实保留了关键字“pure”,因此在vNext中可能可以手动标记方法为纯净,从而允许更积极地缩减lambda表达式的图形。
但是,如果您使用记录或代数类型,F#将创建默认比较和相等运算符,并提供复制语义。除了许多其他好处(模式匹配,封闭世界假设),这也减轻了重要负担!
是的,如果你不考虑F#,但考虑例如Haskell。没有副作用的事实确实为优化带来了很多可能性。
例如,在类似C语言中考虑:
int factorial(int n) {
if (n <= 0) return 1;
return n* factorial(n-1);
}
int factorialuser(int m) {
return factorial(m) * factorial(m);
}
很遗憾,由于F#只是基本纯净的,因此并没有太多机会进行积极优化。事实上,有些地方F#比C#“悲观”,例如制作防御性结构体副本以防止可观察到的突变。但好的一面是,尽管如此,编译器总体表现良好,在大多数情况下提供与C#相当的性能,同时使程序更易于理解。
有时候,对于函数式语言的额外优化是可能的,但并不一定是因为不可变性。在内部,许多编译器会将代码转换成SSA(单静态赋值)形式,其中函数内的每个本地变量只能分配一次。这可以用于命令式和函数式语言。例如:
x := x + 1
y := x + 4
可以变成
x_1 := x_0 + 1
y := x_1 + 4
其中x_0
和x_1
是不同的变量名。这大大简化了许多转换操作,因为您可以将代码块移动到程序中的特定点而不必担心它们在该点具有的值是什么。但对于存储在内存中的值(即全局变量、堆值、数组等),这种方法不起作用。同样,这适用于函数式和命令式语言。
大多数函数式语言提供的一个好处是强类型系统。这使得编译器能够做出否则无法做出的假设。例如,如果您有两个不同类型的引用,编译器知道它们不能别名(指向同一物体)。这不是C编译器能够做出的假设。
...f(x)...f(x)...
。 但是,这种分析很难进行,没有非常精确的信息,因为F#运行在.Net运行时上,而.Net没有将方法标记为纯(无副作用)的方法,因此即使要尝试执行任何此类操作也需要大量内置信息和分析。