Javac之类的编译器会自动检测纯函数并将其并行化吗?


12

已知纯函数可促进并行化。 函数式编程在本质上适合于并行执行是什么呢?

Javac之类的编译器是否足够聪明,可以检测方法何时是纯函数?人们总是可以实现实现诸如Function之类的功能接口的类,但会产生副作用。


7
问题不仅在于编译器是否可以知道函数是否为纯函数,还在于它是否可以智能地调度并行执行的纯函数。仅为每个线程激发一个新线程是不够的:这效率低下。GHC(Haskell)使用懒惰和“绿色线程”处理此问题;老实说,如果还要尝试使用任何不纯的语言,我会感到惊讶,因为要确保相对于主要的不纯线程正确地安排了纯线程,还存在额外的困难。
瑞安·赖希

@RyanReich,以不纯函数式语言(例如Java)使用函数式编程会有什么性能提升?功能编程的收益是纯功能性的,例如模块化吗?
Naveen

@RyanReich GHC通过让程序员在需要并行处理时进行注释来解决该问题。纯粹意味着这些注释永远不会改变语义,只会改变性能。(也有并发机制可以引起并行性,但这是一条不同的鱼。)
德里克·埃尔金斯

@Naveen除了并行性之外,纯函数在优化方面还有其他好处,例如更大的自由性重新排序代码,备忘录和公共子表达式消除。我可能是错的,但是我怀疑javac会尝试检测纯度,因为它在惯用代码中可能很少见,除了最琐碎的情况以外,对于所有情况而言都有些困难。例如,您需要知道不会有NullPointerExceptions。对于典型的Java应用程序,基于此进行优化的好处可能也很小。
德里克·埃尔金斯

6
javac是Java编译器,它接收Java源代码并生成Java字节码类文件。它可以(应该做)的事情受到很大的限制。它不具有将并行性引入字节码类文件的自由或必要的基础机制。
Erik Eidt

Answers:


33

是诸如Javac这样的编译器,它们足够聪明,可以检测方法何时是纯函数。

这不是“足够聪明”的问题。这称为纯度分析,在一般情况下证明是不可能的:它等效于解决停止问题。

当然,现在,优化器一直在执行无法证明的事情,“在一般情况下无法证明”并不意味着它永远不会起作用,而仅意味着它不能在所有情况下都能起作用。因此,实际上有算法来检查一个函数是否纯净,结果常常是“我不知道”,这意味着出于安全性和正确性的考虑,您需要假设该特定功能可能不纯净。

即使在它确实起作用的情况下,算法也是复杂且昂贵的。

因此,这就是问题1:它仅适用于特殊情况

问题2:图书馆。为了使函数成为纯函数,它只能调用纯函数(而那些函数只能调用纯函数,依此类推)。Javac显然只知道Java,并且只知道它可以看到的代码。因此,如果您的函数在另一个编译单元中调用一个函数,则您将无法知道它是否是纯函数。如果它调用用另一种语言编写的函数,您将不会知道。如果它在库中甚至尚未安装的函数中调用函数,您将不知道。等等。

仅当您进行整个程序分析时,整个程序都用相同的语言编写,并且一次全部编译时,这才起作用。您不能使用任何库。

问题3:安排时间。一旦弄清了哪些部分是纯的,您仍然必须将它们安排在单独的线程中。或不。启动和停止线程非常昂贵(尤其是在Java中)。即使保留线程池并且不启动或停止线程池,线程上下文切换也很昂贵。您需要确保计算的运行时间明显长于调度和上下文切换所需的时间,否则您将失去性能而不是获得性能。

正如您现在可能猜到的那样,在一般情况下,弄清楚计算将花费多长时间是不可能的(我们甚至无法弄清楚是否将花费有限的时间,更不用说要花费多少时间了),即使在特殊情况。

除了:Javac和优化。请注意,大多数Javac实现实际上并没有执行很多优化。例如,Oracle的javac实现依赖底层执行引擎进行优化。这导致了另一组问题:例如,javac认为某个特定的函数是纯函数并且足够昂贵,因此它将其编译为在其他线程上执行。然后,平台的优化器(例如,HotSpot C2 JIT编译器)出现并优化整个功能。现在,您有一个空线程不执行任何操作。或者,再次想像一下,javac决定在另一个线程上调度函数,而平台优化器可以 完全优化它,除非它无法跨线程边界执行内联,因此现在可以不必要地执行可以完全优化的功能。

因此,只有在一个编译器一次性完成大部分优化的情况下,这样做才有意义,以便编译器了解并可以利用不同级别的所有不同优化以及它们之间的相互作用。

请注意,例如,HotSpot C2 JIT编译器实际上执行一些自动矢量化,这也是自动并行化的一种形式。


好吧,根据您对“纯函数”的定义,可以允许在实现中使用不纯函数。
重复数据删除器

@Deduplicator嗯,这取决于你的定义definition,使用不同definitionpurity可能是模糊的

1
问题#2大部分由于几乎所有优化都由JIT执行(您显然知道但忽略了)这一事实而无效。类似地,由于JIT严重依赖于解释器收集的统计信息,因此问题3的一部分变得无效。我特别不同意“您不能使用任何库”,这是因为救援工作的优化不足。我同意增加的复杂性将是一个问题。
maaartinus

2
@maaartinus:此外,我回答的最后部分是特定于javac的。例如,我确实确实提到:“只有当您对整个程序进行分析,整个程序以相同的语言编写并且一次全部编译后,此方法才有效。” 对于C2显然是正确的:它仅处理一种语言(JVM字节码),并且可以一次访问整个程序。
约尔格W¯¯米塔格

1
@JörgWMittag我知道OP询问Javac,但是我敢打赌他们假设Javac是负责优化的事情。而且他们几乎不知道有C2。我不是说,你的答案是不好的。只是让javac进行任何优化(除了使用琐碎之类的琐事外StringBuilder)都是无稽之谈,因此我将其忽略,仅假设OP编写javac但表示Hotspot。您的问题2是反对在Javac中进行任何优化的一个很好的理由。
maaartinus

5

投票赞成的答案未能说明一件事。线程之间的同步通信非常昂贵。如果该函数能够以每秒数百万次调用的速度执行,那么实际上并行化它而不是保持原样会对您造成更大的伤害。

不幸的是,使用带有原子变量的繁忙循环的同步线程间通信的最快形式是能量效率低的。如果必须使用条件变量来节省能量,则线程间通信的性能会受到影响。

因此,编译器不仅需要确定函数是否是纯函数,还需要估计函数的执行时间以查看并行化是否是净赢。同样,它需要在使用原子变量或条件变量的繁忙循环之间进行选择。这将需要在后台创建线程。

如果动态创建线程,它甚至比使用条件变量还要慢。因此,编译器将需要设置一定数量的已在运行的线程。

因此,您的问题的答案是否定的,编译器不够“智能”,无法自动并行化纯函数,尤其是在Java世界中。他们很聪明,没有自动将它们并行化!


5
它们很聪明,没有自动将它们并行化!:太过分了。的确,仅出于自身的原因在每个可能的点进行并行处理通常都是低效的,但智能编译器会确定一种实用的并行化策略。我认为大多数人都理解这一点,所以当我们谈论自动并行化时,我们指的是自动实际并行化。
纳特

@Nat:太难了。这将需要在100毫秒的运行时范围内标识纯函数,并期望编译器对循环的运行时有任何想法,这些循环在其迭代中没有常量(以及您不希望的情况)是愚蠢的。
约书亚

我同意-@Nat的评论暗示并行化不一定意味着有多个线程,这是事实。例如,在某些情况下,JIT可以内联多个调用一个纯函数,并交织其CPU指令。例如,如果两个方法都调用获取常量,则可以将其获取一次并保存在CPU寄存器中,以供方法的两个实例使用。现代CPU是具有众多通用寄存器和专用指令的野兽,它们在优化代码时会非常有用。

1
@Joshua:对于JIT编译器确实要容易得多。JIT编译器还可以弄清楚一个函数可能不是纯函数,但是到目前为止,没有调用调用过不纯行为。
gnasher729

我同意@Joshua。我的工作很难并行化。我试图手动并行化它,甚至通过做一些简化的近似(从而修改算法),也每次都失败了。即使一个程序告诉并行处理是否可行也是非常困难的,即使比实际并行处理要简单得多。记住,我们在谈论图灵完备的编程语言。
juhist
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.