Haskell 中的半显式并行

Posted

技术标签:

【中文标题】Haskell 中的半显式并行【英文标题】:Semi-explicit parallelism in Haskell 【发布时间】:2013-09-01 19:31:32 【问题描述】:

我正在阅读 性,但有些困惑。

标准杆 :: a -> b -> b

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

1) 它创建了太多的小项目,无法有效地安排。据我了解,如果你对 Haskell 程序的每一行都使用 par 函数,它会创建太多线程,而且根本不实用。对吗?

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

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

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

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

【问题讨论】:

【参考方案1】:

您提到的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 执行上下文)实现并行图缩减并分别维护一个火花池和一个线程池。通常每个核心有一个能力(可以被认为是操作系统线程)。在连续体的另一端有火花,它们基本上是指向图中尚未评估的部分的指针(thunk)。并且有线程(实际上是任务或工作单元),因此要并行评估,火花需要变成一个线程(它有一个所谓的线程状态对象,它包含必要的执行环境并允许一个 thunk并行评估)。一个 thunk 可能依赖于其他 thunk 和块的结果,直到这些结果到达。这些线程比 OS 线程更轻量级,并且被多路复用到可用的 Capablities 上。

因此,总而言之,运行时甚至不需要创建轻量级线程来评估子表达式。顺便说一句,随机工作窃取用于负载平衡。

这是一种非常高级的并行方法,可以通过设计避免竞争条件和死锁。同步是通过图形缩减隐式介导的。一个不错的立场声明进一步讨论了Why Parallel Functional Programming Matters。有关幕后抽象机器的更多信息,请查看 Stackless Tagless G-Machine 并查看 GHC wiki 上 Haskell Execution Model 上的注释(通常是源代码旁边的最新文档)。

【讨论】:

我想我不太明白你所说的图形缩减是什么意思 让我们采用表达式1 + (inc 2),其中inc是增量函数,然后计算可以用一棵树来表示,其中操作或函数应用程序是节点,每个叶子都有简单的值。因为haskell避免了工作重复,一些节点可能指向其他子树,所以我们有一个图。然后,图形缩减是将函数应用于运算符以将图形缩减为结果值的过程。例如1 + (inc 2) -> 1 + 3 -> 4;这个视频可能有助于形象化:youtube.com/watch?v=ZebxyrCb1ug 上面写着:将函数应用于参数。图形缩减有点误导,因为图形实际上可能会在过程中增长,但最终会缩减为结果。 (名称与 FP 所基于的 lamda 演算定义的运算有关)【参考方案2】:

    是的,你是对的。通过为要计算的每个表达式创建火花,您不会获得任何收益。你会得到方法,太多的火花。试图管理这就是Data Parallel Haskell 的意义所在。 DPH 是一种将嵌套计算分解为大小合适的块的方法,然后可以并行计算。请记住,这仍然是一项研究工作,可能还没有为主流消费做好准备。

    再一次,你是对的。如果 a 依赖于 b,您必须计算 b 所需的尽可能多的 a 才能开始计算 b。

    是的。与某些替代方案相比,线程实际上具有相当高的开销。 Sparks 有点像 thunk,只是它们可以独立于时间进行计算。

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

par 只会产生火花。火花不是线程。火花占用一个工作池,调度程序执行工作窃取——即当一个线程空闲时,它从池中获取一个火花并计算它。

【讨论】:

嗯,sparking的意思是主线程会创建一个任务,然后把这个任务放到工作池中。如果 RTS 有 6 个线程,其中一个可用,这个线程会在池中挑选一个任务来评估,对吗?顺便说一句,“评估”相当于“计算”或“评估”只是衡量执行的时间,而不是真正计算它? 评估与计算是一回事。评估是大多数 FP 人所说的,但当我与新的 Haskeller 交谈时,我倾向于称它为计算。有时我会搞砸并在同一个句子中说两者。

以上是关于Haskell 中的半显式并行的主要内容,如果未能解决你的问题,请参考以下文章

Haskell 中的并行“任何”或“全部”

为啥在 Haskell 中显式推导 Show/Read?

为啥 Haskell 中没有隐式并行性?

Haskell 中的参数化类型

在 Haskell 中使用并行策略时速度变慢

如何在haskell快速傅里叶变换中应用数据并行?