是因为我编写不同的东西还是因为编译的方式不同,所以函数编程在多线程中是否更快?


63

我将深入研究函数式编程的世界,并且不断阅读有关函数式语言更适合多线程/多核程序的内容。我了解函数式语言如何做很多不同的事情,例如递归随机数等,但是我似乎无法弄清楚函数式语言中的多线程是否更快,因为它的编译方式不同或因为我编写的方式不同。

例如,我已经用Java编写了实现某种协议的程序。在此协议中,双方互相发送和接收数千条消息,它们对这些消息进行加密并一次又一次地重新发送(并接收)。不出所料,当处理成千上万的规模时,多线程是关键。在此程序中,没有锁定

如果我在Scala(使用JVM)中编写相同的程序,这种实现会更快吗?如果是,为什么?是因为写作风格吗?如果因为写作风格,现在Java包含了lambda表达式,我不能将Java与lambda一起使用来达到相同的结果吗?还是因为Scala将以不同的方式编译事物而更快?


64
Afaik函数式编程不能使多线程更快。它使多线程更易于实现和更安全,因为功能编程的某些功能(例如不变性)和不具有副作用的功能对此没有帮助。
Pieter B

7
请注意,1)并未真正定义更好; 2)肯定 不是简单定义为“更快”。对于X来说,要求X的代码大小达到十亿倍以实现0.1%的性能提升的语言X并不比Y更好。
巴库里

2
您是要问“功能编程”还是“以功能风格编写的程序”?通常,更快的编程不会产生更快的程序。
Ben Voigt

1
别忘了总是有一个GC必须在后台运行并满足您的分配需求...而且我不确定它是多线程的...
Mehrdad

4
这里最简单的答案是:函数式编程允许编写考虑较少竞争条件问题的程序,但这并不意味着编写命令式风格的程序会较慢。
戴维德·普拉

Answers:


97

人们之所以说功能语言更适合并行处理,是因为它们通常避免可变状态。可变状态是并行处理中的“万恶之源”。当它们在并发进程之间共享时,它们真的很容易陷入竞争状态。如前所述,争用条件的解决方案涉及锁定和同步机制,这会导致运行时开销,因为进程相互等待以利用共享资源,并且设计复杂性更高,因为所有这些概念都容易成为现实。深入嵌套在此类应用程序中。

当避免可变状态时,对同步和锁定机制的需求也随之消失。因为函数语言通常避免可变状态,所以它们自然会更有效地用于并行处理-您将不会有共享资源的运行时开销,也不会拥有通常会增加的设计复杂性。

但是,这都是偶然的。如果您的Java解决方案还避免了可变状态(特定于线程之间共享),则将其转换为Scala或Clojure之类的功能语言不会对并发效率产生任何好处,因为原始解决方案已经消除了由以下原因引起的开销:锁定和同步机制。

TL; DR:如果Scala中的解决方案在并行处理方面比Java中的解决方案更有效,那不是因为代码的编译或通过JVM运行的方式,而是因为Java解决方案在线程之间共享可变状态,要么导致争用条件,要么增加同步的开销,以避免它们。


2
如果只有一个线程修改一条数据;无需特别护理。只有当多个线程可以修改同一数据时,您才需要某种特殊的照顾(同步,事务性内存,锁定等等)。这样的一个例子是线程的堆栈,该堆栈不断地被功能代码所突变,但未被多个线程修改。
布伦丹

31
一个线程使数据发生变异,而其他线程则读取数据,这足以使您开始“特别注意”。
彼得·格林

10
@Brendan:否,如果一个线程修改数据而其他线程正在从同一数据读取数据,则说明您处于竞争状态。即使只修改一个线程,也需要特别注意。
Cornstalks

3
在并行处理的上下文中,可变状态是“万恶之源” =>如果您还没有看过Rust,我建议您先看一下它。通过意识到真正的问题与别名混在一起是可变的,它设法非常有效地允许可变性:如果您只有别名或只有可变性,就没有问题。
Matthieu M.

2
@MatthieuM。是的,谢谢!我进行了编辑,以使答案中的内容更加清晰。当可变状态在并发进程之间共享时,可变状态只是“万恶之源”-Rust通过其所有权控制机制避免了这种情况。
MichelHenrich '16

8

两者都有。它之所以更快,是因为它更容易以更容易编译的方式编写代码。切换语言不一定会带来速度上的差异,但是如果您是从一种功能语言开始的,则可以以更少的程序员精力完成多线程。同样,对于程序员而言,犯线程错误很容易,这将使命令性语言的运行速度增加,并且发现这些错误也变得更加困难。

原因是当务之急,程序员通常会尝试将所有无锁的线程化代码放在尽可能小的盒子中,并尽快对其进行转义,以回到舒适的可变同步世界中。花费您速度的大多数错误都是在该边界接口上犯的。在函数式编程语言中,您不必担心在该边界上犯错误。可以这么说,您的大多数调用代码也都位于“框内”。


7

通常,函数式编程不能使程序更快。它的目的是简化并行和并发编程。有两个主要键:

  1. 避免可变状态往往会减少程序中可能出错的事物的数量,在并发程序中甚至更是如此。
  2. 避免使用共享内存和基于锁的同步原语,而使用高级概念,往往会简化代码线程之间的同步。

关于点2的一个很好的例子是,在Haskell中,我们在确定性并行非确定性并发之间有着明显的区别。没有比引用西蒙·马洛(Simon Marlow)出色的著作《Haskell中的并行和并发编程》更好的解释了(引自第1章):

并行程序是一种使用计算硬件(例如,若干个处理器核心)的多个更快速地执行计算。目的是通过将计算的不同部分委派给同时执行的不同处理器,从而尽早获得答案。

相反,并发是一种程序结构技术,其中有多个控制线程。从概念上讲,控制线程“同时”执行;即,用户看到了他们的效果交错。它们是否实际上同时执行是实现细节。并发程序可以通过交错执行在单个处理器上执行,也可以在多个物理处理器上执行。

除此之外,Marlow还提到了确定性的维度:

相关的区别是之间的确定性不确定性的编程模型。确定性编程模型是其中每个程序只能给出一个结果的模型,而非确定性编程模型则根据执行的某些方面允许可能具有不同结果的程序。并发编程模型不一定是确定性的,因为它们必须与在无法预测的时间导致事件的外部代理进行交互。但是,不确定性有一些明显的缺点:程序变得更加难以测试和推理。

对于并行编程,我们将尽可能使用确定性编程模型。由于目标只是为了更快地找到答案,因此我们宁愿不要使我们的程序在此过程中难以调试。确定性并行编程是两全其美的方法:可以在顺序程序上执行测试,调试和推理,但是在增加更多处理器的情况下,程序运行速度更快。

在Haskell中,围绕这些概念设计了并行性和并发功能。特别是,与其他语言归为一个功能集一样,Haskell分为两种:

  • 确定性功能和并行性库。
  • 非确定性功能和并发库。

如果您只是想加速纯粹的确定性计算,那么具有确定性并行性通常会使事情变得容易得多。通常,您只需要执行以下操作:

  1. 编写一个产生答案列表的函数,每个答案的计算成本都很高,但彼此之间并不太依赖。这是Haskell,因此列表是惰性的 -直到消费者要求它们时才真正计算其元素的值。
  2. 使用Strategies库可以跨多个内核并行使用函数的结果列表元素。

几周前,我实际上是通过我的一个玩具项目程序做到这一点的。并行化程序是微不足道的-实际上,我要做的关键是添加一些代码,指出“并行计算此列表的元素”(第90行),并且在我一些较昂贵的测试用例。

我的程序是否比使用传统的基于锁的多线程实用程序要快?我非常怀疑。就我而言,整洁的事情是花了这么少的钱—我的代码可能不是很理想,但是因为它是如此容易并行化,所以与正确地进行性能分析和优化相比,我以更少的精力获得了很大的加速,而且没有比赛条件的风险。我想这就是函数式编程允许您编写“更快”的程序的主要方式。


2

在Haskell中,如果不通过修改库获取特殊的可修改变量,则修改实际上是不可能的。取而代之的是,函数在创建它们所需的变量时会同时创建它们的值(它们是延迟计算的),并在不再需要时收集垃圾。

即使确实需要修改变量,通常也可以通过备用使用以及不可修改的变量来获得。(haskell中的另一件好事是STM,它用原子操作代替了锁,但是我不确定这是否仅用于函数式编程。)通常,程序的一部分仅需要并行处理以改善性能。性能方面。

这使Haskell中的并行性在很多时候变得容易,并且实际上正在努力使其自动化。对于简单的代码,并行性和逻辑甚至可以分开。

同样,由于在Haskell中评估顺序无关紧要,编译器只是创建一个需要评估的事物队列,并将其发送到任何可用的内核,因此您可以创建一堆不需要的“线程”直到必要时才真正成为线程。评估顺序无关紧要是纯度的特征,通常需要进行功能编程。

进一步阅读
Haskell中的并行性(HaskellWiki)
“真实世界中的Haskell”中的并行和多核编程,
Simon Marlow的Haskell中的并行和并行编程


7
grep java this_postgrep scala this_postgrep jvm this_post没有返回任何结果:)
Andres F.

4
问题很模糊。在标题和第一段,它提出关于函数式编程一般,在第二和第三段,它会询问关于Java和Scala 特别。这是不幸的,特别是因为Scala的核心优势之一就是它不是(只是)一种功能语言。马丁·奥德斯基(Martin Odersky)称其为“后功能”,其他人则称其为“对象功能”。术语“功能编程”有两种不同的定义。一个是“编程与一流的程序”(适用于LISP最初的定义),另一种是...
约尔格W¯¯米塔格

2
“使用参照透明,纯净,无副作用的函数和不变的持久性数据进行编程”(更严格,而且更新的解释)。这个答案解决了第二种解释,这很有意义,因为a)第一种解释与并行性和并发性完全无关,b)第一种解释已经变得毫无意义,因为除了C以外,几乎所有语言都在适度的广泛使用下使用今天拥有一流的程序(包括Java),以及c)OP询问有关Java和Scala的区别,但没有...
约尔格W¯¯米塔格

2
关于定义#1,两者之间只有定义#2。
约尔格W¯¯米塔格

评估内容并不像这里写的那么真实。默认情况下,运行时根本不使用多线程,即使IIRC启用了多线程,您仍然必须告诉运行时它应该并行评估哪些内容。
立方
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.