Haskell中的半显式并行性

3

我正在学习Haskell中的半显式并行性,并且有些困惑。

      par :: a -> b -> b

人们说,这种方法允许我们通过并行计算Haskell程序的每个子表达式来实现自动并行化。但是这种方法有以下缺点:

1)它创建了太多的小工作项,不能有效地进行调度。据我所知,如果你对Haskell程序的每行都使用par函数,那么它将创建太多的线程,这根本不切实际。这是正确的吗?

2)使用这种方法,数据依赖关系会限制并行性。如果我理解正确,这意味着每个子表达式必须是独立的。就像在par函数中,a和b必须是独立的。

3)Haskell运行时系统不一定会创建一个线程来计算表达式a的值。相反,它会创建一个spark,有可能在父线程之外的不同线程上执行。

所以,我的问题是:最终运行时系统会创建一个线程来计算a吗?或者如果需要计算表达式b,则系统会创建一个新线程来计算a吗?否则,它将不会创建。这是正确的吗?

我是Haskell的新手,所以也许我的问题对于你们所有人来说仍然很基础。谢谢你的回答。

2个回答

8
你提到的par组合子是Glasgow parallel Haskell(GpH)的一部分,它实现了半显式并行性,这意味着它不是完全隐式的,因此不能提供自动并行化。程序员仍然需要识别被认为值得并行执行的子表达式,以避免你在1)中提到的问题。
此外,注释不是指示性的(如C语言中的pthread_create或Haskell中的forkIO),而是建议性的,也就是说,运行时系统最终决定是否并行评估子表达式。这提供了额外的灵活性和动态粒度控制的方法。此外,所谓的Evaluation Strategies已经被设计来抽象出parpseq,并将协调规范与计算分离。例如,一个策略parListChunk允许对列表进行分块,并将其强制转换为弱头正常形式(这是需要一些严格性的情况)。

2) 并行性受数据依赖的限制,因为计算定义了图形被缩减的方式以及在哪个点上需要哪种计算。并不是每个子表达式都必须独立。例如,E1 par E2返回E2的结果,这意味着为了有用,E1的一部分需要在E2中使用,因此E2依赖于E1。

3) 这里的图片略有混淆,因为它使用了GHC特定的术语。执行并行图形缩减并维护火花池和线程池的能力(或Haskell执行上下文)通常每个核心有一个能力(可以视为操作系统线程)。在另一端的连续体中,火花基本上是指尚未评估的图形部分(惰性求值)。还有线程(实际上是任务或工作单元),为了并行评估火花,需要将其转换为线程(具有所谓的线程状态对象,其中包含必要的执行环境,并允许惰性求值并行评估)。惰性求值可能依赖于其他惰性求值的结果,并阻塞直到这些结果到达。这些线程比操作系统线程轻得多,并且正在复用可用的能力。

总之,运行时甚至不一定会创建轻量级线程来评估子表达式。顺便说一下,随机工作窃取算法用于负载平衡。

这是一种非常高级的并行处理方法,并通过设计避免了竞争条件和死锁。同步通过图形缩减隐式地进行中介。一个很好的立场声明进一步讨论了 为什么并行函数编程很重要。有关幕后抽象机器的更多信息,请查看 无栈标记G机器,并在 GHC wiki 上查看 Haskell 执行模型 的注释(通常是最新的文档与源代码一起)。


我觉得我并不真正理解你所说的图形缩减是什么意思。 - chipbk10
让我们来看一个表达式 1 + (inc 2),其中 inc 是增量函数,那么计算可以用带有操作或函数应用的节点以及每个叶子上的简单值的树来表示。因为 Haskell 避免了工作重复,一些节点可能指向其他子树,所以我们有一个图。图还原是将函数应用于运算符以将图还原为结果值的过程。例如:1 + (inc 2) -> 1 + 3 -> 4;这个视频可以帮助您可视化此过程:http://www.youtube.com/watch?v=ZebxyrCb1ug - jev
上面写着:“将函数应用于参数”。图形缩减有点误导,因为在过程中图形实际上可能会增长,但最终会被缩减为结果。(该名称与 FP 基于的 lambda 演算定义的操作有关) - jev

3
  1. 没错,你说得对。对于每个想要计算的表达式都创建一个Spark并不会有任何好处。你会得到太多的Sparks,试图管理这些Sparks是 Data Parallel Haskell 项目致力于解决的问题。DPH 是一种将嵌套计算分解成大小适当的块的方法,然后可以并行计算这些块。请记住,这仍然是研究性工作,可能还没有准备好供主流使用。

  2. 再次确认,如果a依赖于b,则必须计算b需要的a的尽可能多的部分才能启动b的计算。

  3. 对的。相比其他一些替代方案,线程的开销实际上相当高。Sparks 类似于thunks,只是它们可以在时间上独立地计算。

不,运行时系统(RTS)不会创建一个线程来计算a。您可以决定 RTS 应该运行多少个线程(例如:+RTS -N6 表示六个线程),并且它们将在程序运行期间保持活动状态。

par 只会创建一个Spark,而 Spark 不是线程。这些Sparks占用一个工作池,调度程序执行工作窃取——即当一个线程空闲时,它从池中获取一个Spark并计算它。


点燃意味着主线程将创建一个任务,然后将该任务放入工作池中。如果 RTS 有 6 个线程,并且其中一个可用,那么这个线程将选择池中的一个任务进行评估,对吗?顺便说一下,“评估”等同于“计算”或“求值”,只是衡量执行时间,而不是真正计算它? - chipbk10
1
评估和计算是同一件事情。大多数函数式编程人员会称之为评估,但当我与新手Haskeller交谈时,我倾向于称之为计算。有时我会在同一个句子中混淆两者。 - kqr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接