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

Posted

技术标签:

【中文标题】为啥 Haskell 中没有隐式并行性?【英文标题】:Why is there no implicit parallelism in Haskell?为什么 Haskell 中没有隐式并行性? 【发布时间】:2013-02-06 23:28:45 【问题描述】:

Haskell 是函数式且纯粹的,因此基本上它具有编译器能够处理 implicit parallelism 所需的所有属性。

考虑这个简单的例子:

f = do
  a <- Just 1
  b <- Just $ Just 2
  -- ^ The above line does not utilize an `a` variable, so it can be safely
  -- executed in parallel with the preceding line
  c <- b
  -- ^ The above line references a `b` variable, so it can only be executed
  -- sequentially after it
  return (a, c)
  -- On the exit from a monad scope we wait for all computations to finish and 
  -- gather the results

执行计划的示意图可以描述为:

               do
                |
      +---------+---------+
      |                   |
  a <- Just 1      b <- Just $ Just 2
      |                   |
      |                 c <- b
      |                   |
      +---------+---------+
                |
           return (a, c)

为什么编译器中还没有使用标志或编译指示实现这样的功能?有哪些实际原因?

【问题讨论】:

do rc1 &lt;- system("/usr/games/tetris") ; rc2 &lt;- system("rm -rf /") ?? 因为您在 Maybe monad 中,所以在您的 do 块中有 ba 的隐式依赖。 b &lt;- ... 只会在a 未绑定到Nothing 的情况下执行。 @NikitaVolkov 实际上,我的回答可以解释为对 n.m. 的支持。从某种意义上说,尝试评估绑定到 b 的表达式是安全的,但可能不会使用该结果。 @sabauma 哦,对,误会你了。但是b 可以乐观地计算,因为知道在mzero 的情况下它将被丢弃。是的,开销,但我们谈论的是编译器选项,而不是默认行为。 IMO 为节省开发时间付出的代价是值得的。除了这只是一个例子,这里还有另一个:map (+2) [0,1,2,3] 我认为值得注意的是,即使没有 Haskell 编译器提供隐式并行性(据我所知),也有一些库可以提供,例如repa。我认为 sabauma 的回答是正确的:如果没有一些额外的领域知识,那么隐式并行性何时是有利的,这是一个悬而未决的问题。 【参考方案1】:

虽然由于隐含数据,您的代码块可能不是最好的示例 ab之间的依赖关系,值得注意的是这两个 绑定在其中通勤

f = do
  a <- Just 1
  b <- Just $ Just 2
  ...

会给出相同的结果

f = do
  b <- Just $ Just 2
  a <- Just 1
  ...

所以这仍然可以以推测的方式并行化。值得一提的是 这不需要与 monads 有任何关系。例如,我们可以评估 let-block 中的所有独立表达式并行,或者我们可以引入一个 let 的版本会这样做。 Common Lisp 的 lparallel library 就是这样做的。

现在,我绝不是这方面的专家,但这是我的理解 的问题。 一个主要的绊脚石是确定何时并行化 多个表达式的评估。有与启动相关的开销 用于评估的单独线程,并且如您的示例所示,它可能会导致 在浪费的工作中。某些表达式可能太小而无法进行并行计算 值得开销。据我了解,提出将是一个完全准确的指标 表达式的成本相当于解决停止问题,所以 您只能使用启发式方法来确定要做什么 并行计算。

那么在一个问题上投入更多的核心并不总是更快。即使当 使用许多可用的 Haskell 库显式并行化问题, 仅仅通过并行计算表达式,您通常不会看到太多的加速 由于大量的内存分配和使用以及这给垃圾带来的压力 收集器和 CPU 缓存。你最终需要一个漂亮的紧凑内存布局,并且 智能地遍历您的数据。有 16 个线程遍历链表将 只是在内存总线上成为瓶颈,实际上可能会使事情变慢。

至少,可以有效并行化的表达式是 对许多程序员来说并不明显(至少对这个来说不是),所以让一个编译器来 有效地做到这一点并非易事。

【讨论】:

只是为了澄清:lparallel 确实有 plet 并行评估表达式,但您提供的链接是关于优化 plet 以实现加速的方式,即使评估这些表达式很便宜。无需提示的隐式并行实际上可以使用Cilk 和类似的实现,如lparallel 中的实现。【参考方案2】:

这是一个长期研究的话题。虽然您可以在 Haskell 代码中隐式推导出并行性,但问题是对于当前硬件而言并行性太多,粒度太细。

所以你最终会花精力记账,而不是更快地运行。

由于我们没有无限的并行硬件,所以关键在于选择正确的粒度 - 也是 粗,会有空闲处理器,太细和开销 将是不可接受的。

我们拥有的是更粗粒度的并行性 (sparks),适用于生成数千或数百万个并行任务(因此不是在指令级别),它们映射到我们今天通常可用的少数核心。

请注意,对于某些子集(例如数组处理),存在具有严格成本模型的全自动并行化库。

有关这方面的背景信息,请参阅 Feedback Directed Implicit Parallelism,他们介绍了一种在任意 Haskell 程序中插入 par 的自动化方法。

【讨论】:

"A Gentle Introduction to GPH" 也是一本很好的关于 Haskell 中隐式和显式并行性的阅读材料。【参考方案3】:

简短的回答:有时并行运行的东西会变得更慢,而不是更快。弄清楚什么时候是好主意,什么时候不是一个好主意是一个悬而未决的研究问题。

但是,您仍然可以“突然利用所有这些内核,而不必担心线程、死锁和竞争条件”。这不是自动的;你只需要给编译器一些关于在哪里做的提示! :-D

【讨论】:

【参考方案4】:

其中一个原因是因为 Haskell 是非严格的,默认情况下它不会评估任何内容。一般来说,编译器不知道ab 的计算会终止,因此尝试计算它会浪费资源:

x :: Maybe ([Int], [Int])
x = Just undefined
y :: Maybe ([Int], [Int])
y = Just (undefined, undefined)
z :: Maybe ([Int], [Int])
z = Just ([0], [1..])
a :: Maybe ([Int], [Int])
a = undefined
b :: Maybe ([Int], [Int])
b = Just ([0], map fib [0..])
    where fib 0 = 1
          fib 1 = 1
          fib n = fib (n - 1) + fib (n - 2)

考虑以下功能

main1 x = case x of
              Just _ -> putStrLn "Just"
              Nothing -> putStrLn "Nothing"

(a, b) 部分不需要评估。一旦你得到 x = Just _ 你就可以继续分支 - 因此它适用于所有值,但 a

main2 x = case x of
              Just (_, _) -> putStrLn "Just"
              Nothing -> putStrLn "Nothing"

此函数强制对元组进行评估。因此x 将因错误而终止,而 rest 将起作用。

main3 x = case x of
              Just (a, b) -> print a >> print b
              Nothing -> putStrLn "Nothing"

此函数将首先打印第一个列表,然后是第二个。它适用于z(导致打印无限的数字流,但 Haskell 可以处理它)。 b 最终会耗尽内存。

现在通常您不知道计算是否终止以及它将消耗多少资源。无限列表在 Haskell 中非常好:

main = maybe (return ()) (print . take 5 . snd) b -- Prints first 5 Fibbonacci numbers

因此,在 Haskell 中生成线程来评估表达式可能会尝试评估一些不打算完全评估的东西——比如所有素数的列表——但程序员将其用作结构的一部分。上面的例子非常简单,你可能会争辩说编译器会注意到它们——但是由于停机问题,通常不可能(你不能编写接受任意程序及其输入并检查它是否终止的程序)——因此它不是安全优化。

此外 - 其他答案提到过 - 很难预测额外线程的开销是否值得参与。即使 GHC 没有使用绿色线程(使用固定数量的内核线程 - 除了一些例外)为 spark 生成新线程,您仍然需要将数据从一个核心移动到另一个核心并在它们之间进行同步,这可能会非常昂贵。

但是,Haskell 确实通过 par 和类似函数在不破坏语言纯度的情况下引导了并行化。

【讨论】:

【参考方案5】:

实际上有这样的尝试,但由于可用内核数量较少,因此没有在普通硬件上进行。该项目名为Reduceron。它以高并行度运行 Haskell 代码。如果它以proper 2 GHz ASIC core 的形式发布,我们将在 Haskell 执行速度方面取得重大突破。

【讨论】:

以上是关于为啥 Haskell 中没有隐式并行性?的主要内容,如果未能解决你的问题,请参考以下文章

Haskell 中的半显式并行

Haskell趣学指南

为啥执行计划中的并行性不好[关闭]

编写一次并行数组 Haskell 表达式,在 CPU 和 GPU 上运行 repa 并加速

为啥 Haskell 没有在函数签名中推断数据类型的类型类?

为啥这个 MVC 方法没有以并行方式运行?