作为这个问题的后续问题What are the advantages of built-in immutability of F# over C#? - 我是否正确假设F#编译器能够在知道它处理大部分不可变代码的情况下进行某些优化?我的意思是即使开发人员写了“Functional C#”,编译器也不会知道开发人员试图编写的所有不可变性,因此它无法进行相同的优化,对吧?
一般来说,函数式语言的编译器能够进行使用命令式语言无法实现的优化 - 即使是尽可能多的不可变写的语言 -
答案 0 :(得分:20)
我认为F#编译器可以确定是正确的 知道它处理很大程度上不可变的代码的优化吗?
不幸的是没有。对于编译器编写者来说,“很大程度上不可变”和“不可变”之间存在巨大差异。即使是保证不变性对于优化器也不是那么重要;它买的主要是你可以写一个very aggressive inliner。
一般来说,函数式语言的编译器能够进行使用命令式语言无法实现的优化 - 即使是尽可能多的不可变写的语言 -
是的,但这主要是能够在更多地方更轻松地应用经典优化的问题。例如,不变性使得应用 common-subexpression淘汰变得更加容易,因为不变性可以保证某些内存单元格的内容不会被更改。
另一方面,如果您的函数式语言不仅仅是不可变的,而是纯(没有像I / O这样的副作用),那么您启用了一个涉及重写源级表达式的新类优化更有效的表达方式。其中最重要和最有趣的一点是快捷砍伐,这是一种避免为中间结果分配内存空间的方法。阅读的一个很好的例子是stream fusion。
如果您正在为高性能编译静态类型的函数式语言,这里有一些重点:
有效使用记忆。如果可以的话,使用“未装箱”的值,避免分配和额外的堆间接。特别是流融合和其他森林砍伐技术都非常有效,因为它们消除了分配。
拥有超快速分配器,并通过多次分配来分摊堆耗尽检查。
内联功能有效。特别是跨模块边界的内联小函数。
通常通过闭包转换有效地表示第一类函数。
不要忽视经典的标量和循环优化。它们对TIL和Objective Caml等编译器产生了巨大的影响。
如果您使用像Haskell或Clean这样的惰性函数语言,那么还有许多与thunk相关的特殊事项。
脚注:
完全不变的一个有趣的选择是能够执行非常细粒度的并行性。这个故事的结尾尚未被告知。
为F#编写一个好的编译器比编写一个典型的编译器(如果有这样的东西)更难,因为F#受到如此严格的约束:它必须很好地完成功能,但它必须在.NET框架,它没有考虑到函数式语言。我们非常感谢Don Syme和他的团队在严重受限制的问题上做得非常出色。
答案 1 :(得分:7)
没有
F#编译器不会尝试分析方法或lambda的引用透明性。 .NET BCL根本就不是为此设计的。
F#语言规范确实保留了关键字“pure”,因此可以在vNext中手动将方法标记为纯,从而允许对lambda表达式进行更积极的图形缩减。
但是,如果使用记录或代数类型,F#将创建默认比较和相等运算符,并提供复制语义。在许多其他好处(模式匹配,封闭世界假设)中,这减轻了重大负担!
答案 2 :(得分:5)
是的,如果您不考虑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);
}
如果在Haskell中编写了相应的方法,那么在调用factorialuser时,将无法再次调用factorial 。有可能在C#中做到这一点,但我怀疑当前的编译器是否这样做,即使对于一个简单的例子也是如此。随着事情变得越来越复杂,C#编译器很难优化到Haskell所能达到的水平。
注意,目前F#并不是真正的“纯粹”功能语言。所以,我带来了Haskell(很棒!)。
答案 3 :(得分:3)
不幸的是,因为F#只是大多数纯粹的,所以并没有那么多积极优化的机会。实际上,有些地方F#“与C#相比”使代码变得“悲观”(例如,制作结构的防御性副本以防止可观察到的变异)。从好的方面来说,尽管如此,编译器总体上做得很好,在大多数地方提供了与C#相当的性能,同时使程序更易于推理。
答案 4 :(得分:2)
有时可以对函数式语言进行额外的优化,但不一定是因为不变性。在内部,许多编译器将代码转换为SSA(单个静态赋值)形式,其中函数内的每个局部变量只能分配一次。这可以用于命令式和函数式语言。例如:
x := x + 1
y := x + 4
可以成为
x_1 := x_0 + 1
y := x_1 + 4
其中x_0
和x_1
是不同的变量名称。这极大地简化了许多转换,因为您可以移动一些代码而不必担心它们在程序中的特定点具有什么价值。这不适用于存储在内存中的值(即全局变量,堆值,数组等)。同样,这是针对功能语言和命令式语言完成的。
大多数功能语言提供的一个好处是强类型系统。这允许编译器做出其他情况下无法做出的假设。例如,如果你有两个不同类型的引用,编译器知道它们不能别名(指向同一个东西)。这不是C编译器可能做出的假设。
答案 5 :(得分:2)
我会说'不'。
从不变性或参照透明度中获得的主要“优化”优势是,当您看到...f(x)...f(x)...
之类的代码时,能够执行“公共子表达式消除”。但是如果没有非常精确的信息,很难做到这样的分析,而且由于F#在.Net运行时运行,而.Net无法将方法标记为纯粹(无效),因此需要大量的内置信息和分析。甚至尝试做任何一件事。
另一方面,像Haskell这样的语言(主要是'Haskell',因为像Haskell'这样的语言很少,任何人都听说过或者使用过:),这是懒惰和纯粹的,分析更简单(一切都很纯粹,疯了)。
也就是说,这种“优化”通常可以与系统的其他有用方面(性能可预测性,调试......)进行严重交互。
经常有“足够聪明的编译器可以做X”的故事,但我的观点是“足够聪明的编译器”是,而且永远是一个神话。如果你想要快速代码,那么写快速代码;编译器不会保存你。如果你想要公共子表达式消除,那么创建一个局部变量(自己做)。
这主要是我的意见,欢迎你投票或不同意(事实上,我听说'多核'建议作为一个上升的原因,可能'优化可能再次变得性感',这听起来似乎有道理)。但是,如果您对任何编译器进行任何非平凡优化(源代码中的注释不支持)抱有希望,那么请准备等待很长时间以实现您的希望。
不要误解我的意思 - 不变性很好,可能会帮助你在许多情况下编写“快速”代码。但不是因为编译器优化它 - 而是因为代码易于编写,调试,获取正确,并行化,配置文件,并决定哪些是花费时间的最重要的瓶颈(可能会重写它们)。如果您需要高效的代码,请使用可让您快速开发,测试和分析的开发过程。