功能编程是否允许更好的运行时编译器优化?

时间:2009-12-10 20:04:56

标签: performance functional-programming clojure

注意:已经将此作为Wiki。只要有一个很好的讨论,我不在乎这个问题被标记为什么。

我听说过,因为在纯函数式程序中,没有副作用和值不变异,所以它使编译器更容易进行更多的运行时优化。这到底有多大?

如果这是真的,我的下一个问题是我们为此交易的自由损失是什么?我的意思是,在像C ++ / C这样的语言中,开发人员完全可以控制并且可以调整很多东西。如果我们把这份工作交给编译器,我们就失去了这个机会。这方面的好处是,即使是非专业程序员也可以编写好的代码。此外,如今在机器架构中有如此多的缓存层,甚至可能是专家也无法真正做任何有价值的事情。因此,将此作业委托给比程序员更了解底层架构的编译器是个好主意。

你有什么建议?

10 个答案:

答案 0 :(得分:14)

你见过this "good math, bad math" blog post吗?

我在C,程序集和OCaml中编程。我有时会查看C编译器生成的程序集。 非常低效。我经常注意到一个全局变量一次又一次地被重新加载,当它清楚(对于知道程序逻辑的人)它在执行函数期间不会改变,但是因为函数操纵指针,编译器不能假设变量未被触及。当你注意到这一点时,一种简单的解决方法是在函数开头将全局内容复制到本地,这样你就可以保持C的可移植性,但是获得类似程序集的性能......

但你不应该这样做,事实上,使用更高级别的语言,你也没有,因为有更少的指针,强制转换和其他低级构造,让你接近你想要但实际上妨碍自动优化的硬件。

答案 1 :(得分:9)

要明确,只是因为功能语言的编译器可以更好地进行优化并不意味着它实际上。根据定义,任何疯狂的优化都会导致运行时出现意外行为,这几乎肯定会导致错误。所以要把一个很大的误解放在一边:理论上Haskell程序可以被编译器“免费”并行化,但实际上它们不是也不应该是。

我认为编译器优化成为王者的日子已经过去了。当clockspeed是性能优化的主要瓶颈时,例如循环展开是必不可少的。但是,对于绝大多数应用程序而言,问题不是原始CPU速度,而是其他因素,例如网络带宽,磁盘IO和/或您正在使用的算法的速度。

据我所知,没有编译器优化来重新实现代码以使用并行设计模式。换句话说,如果您使用冒泡排序与每个编译器优化,您可以想到,我正在使用快速排序。在大多数情况下,智能资金都在快速排序实施中。

函数式编程有很多价值,但“因为它们运行得更快”不应该是一个考虑因素。

答案 2 :(得分:9)

  

没有副作用和价值观   不要变异,它会让它变得更容易   编译器使运行时更多   优化

从某种意义上说,是的 - 直到你意识到副作用和可变值本身就是优化。

答案 3 :(得分:5)

在这里查看Haskell与C的比较:

http://www.haskell.org/haskellwiki/Introduction#Quicksort_in_Haskell

你会注意到Haskell版本要短得多。然而,该页面继续说,虽然c版本可能更复杂,但它也可以进行排序,而Haskell版本必须分配更多内存才能工作。

因此,根据定义,如果您希望能够对性能进行极端微调,那么c算法就是更好的方法。

答案 4 :(得分:4)

我认为你应该讨论语言模型而不是编译器优化。势在必行的程序/功能都有自己的发光区域。我将举两个例子:

势在必行 - 程序

Erlang(一种功能语言)。采用共享状态与其内置Mnesia数据库的方法 - 它本身是无功能的,因为更新事务锁定数据库资源获取值,更改它并将其写回。他们这样做是为了表现,因为他们认识到速度在这个领域很重要,如果他们不这样做,Erlang对他们试图解决的问题毫无用处(你能想象每次制作一个完整的数据库文件)改变?)= D.

纯功能

在模型允许的性能方面,功能与非功能是一个有趣的主题领域。从功能的角度出发;当问题本质上是面向并发时,Erlang可以使机器运行的核心饱和。一些测试显示YAWS网络服务器handling 100k connections where Apache fell over at 4k:D Ejabberd还可以处理比传统消息交换机更多的负载。我想说的是,纯函数式语言可以有一个运行时引擎,它可以跨大量内核大规模并行化应用程序。使用命令式过程代码无法轻松完成此任务。

不同的型号适用于不同的问题。我个人认为你可以比功能代码更好地优化程序代码,功能代码仍然作为程序代码执行,并且它被解释:PI已经研究了编译器理论,我真的想不到我会应用的令人兴奋的代码转换我执行它之前的功能代码。你有尾递归和懒惰评估,但肯定是编程模型的特性。

答案 5 :(得分:3)

这是Clojure中的一个示例 - 使用某个函数迭代一系列事物。

(map some-fn big-list)

有什么好处,你可以用一个字符并行化它(多个核心,而不是多个机器):

(pmap some-fn big-list)

如果没有纯函数和不可变数据结构,将无法使pmap工作的语言/编译器机制。

这仍是一项实验性功能。但功能性,无副作用的编程确实使多核执行变得更加容易。

答案 6 :(得分:1)

当谈到C或C ++时,涉及到很多编译器优化,并且编译器可能会决定推翻任何一个不错的小想法,因为它可以更好地确定在特定位置使用哪个优化(例如内联)并将事物作为指令管道等考虑在内。硬件架构对于人们直接编程硬件来说已经变得太复杂了。如今,即使是简单的编译器也会采用大量的优化。使用时髦技巧的可读性较低,编译器难以优化,因为手动优化会降低语义。因此,即使您编写C或C ++,您也可以选择自己拍摄,或让编译器对其进行优化。这就是海湾合作委员会中9,513,227行代码的大部分内容。

我认为,软件具有可验证性,健壮性,可维护性,灵活性和可重用性/可扩展性非常重要。语言越具表现力和简洁性,开发人员就越容易满足这些标准。

最后一个重要的软件标准是:效率。 重要的问题是,你如何确保效率。我会以纯粹的速度和可扩展性来衡量它。这两者并不是相互排斥的,但是使用与并行/分布式计算概念紧密结合的语言,并且就此而言的语言结构设计特征,开发这样的算法要容易得多。当然,如果你自己完成所有的调度,事情会跑得更快,但这很容易出错,而且非常耗时。

您还应该考虑,编译器优化具有线性效应。如果您的算法或系统设计不能很好地扩展,它将无法解决您的问题。如果它可以很好地扩展,你可以通过投入更多硬件来解决性能问题,这总是比开发时间便宜。

最后,你做出的选择是重新发明轮子,让最佳的轮子沿着你想要走的路走,或者拿一个现有的轮子,更关心选择正确的道路,而不是速度,你去的地方。

如果您编写软件,那么提前采取的唯一重要决策就是设计......然后,您实施它,一旦实施,您将能够确定实际的瓶颈,然后在那里进行优化。通常,超过90%的代码在10%的时间内运行较少...优化仅对剩下的10%重要...要确定它们,您需要可以分析的工作软件......而您需要快速,所以你可以重新考虑你的设计,然后再写一些超级优化的东西,否则就废话......

答案 7 :(得分:1)

这只是一个轶事贡献:

我使用Project Euler中的数学问题教自己(一些)Clojure。我使用延迟序列解决了一些问题。

我发现,当我应用类型提示时,长时间计算的性能(启动无关紧要)通常与手工调优的Java相当。

这几乎是预期的:Clojure提前编译JVM字节码,并且运行时与Java代码具有相同的优化运行时。

令我感到惊讶的是,有些计算占据了多个核心,尽管我没有(明知)编程进行任何并行化。似乎运行时能够利用并行化懒惰序列生成和消费的机会。

我的结论:清晰的函数式编程不需要比命令式编程慢,有时可能会找到意想不到的优化或并行化的途径。

作为一个讨厌并发因为它会让我的大脑受到伤害的人,我认为“自动并行化”是一种无价的好处!

答案 8 :(得分:0)

作为一般规则,编译器优化会对以下代码产生影响:

  1. 它实际上是编译的。如果大多数时候调用堆栈的底部是编译器从未看到的代码内部,它可以优化它想要的所有内容,但是没有效果。

  2. 当用户或其他人实际等待程序时,这会消耗很大一部分(如10%或更多)的运行时间。如果没有人在等待,那么优化将永远不会被注意到。

  3. 不包含函数调用。如果正在编译的代码包含函数调用,则程序计数器几乎总是在其他地方。

  4. 所以你永远不会注意到,除非你编写了大量的数据处理大量数据,而没有调用你不编译的代码。就像你将字符串比较放入排序算法的那一刻一样,优化是学术性的。

答案 9 :(得分:0)

理论上,随着编译器越来越智能化,您希望使用高级语言进行编程,因为它们不仅允许编译器进行更多优化,而且还可以使CPU /内存架构本身发展。也许,有一天,我们将拥有"不断发展的CPU",重新安排自己以最好地运行某个应用程序。 C程序假设计算机具有称为Von Neumann体系结构的特定体系结构,而功能/声明性程序更加独立于体系结构,因为您说明需要计算什么而不是如何计算。