为什么Haskell中没有隐式并行性?

时间:2013-02-21 15:13:29

标签: haskell concurrency parallel-processing compiler-optimization

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)

为什么编译器中没有使用flag或pragma实现这样的功能呢?有什么实际的原因?

5 个答案:

答案 0 :(得分:76)

这是一个长期研究的主题。虽然您可以隐式地在Haskell代码中派生并行性,但问题在于,对于当前的硬件来说,存在太多并行性,太细粒度。

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

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

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

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

有关此内容的背景,请参阅http://research.microsoft.com/en-us/um/people/tharris/papers/2007-fdip.pdf,其中介绍了在任意Haskell程序中插入par的自动方法。

答案 1 :(得分:24)

虽然由于隐式数据,您的代码块可能不是最佳示例 ab之间的依赖关系,值得注意的是这两者 绑定在那里通勤

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

将给出相同的结果

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

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

现在,我绝不是这方面的专家,但这是我的理解 这个问题。 一个主要的障碍是确定什么时候并行化是有利的 评估多个表达式。开始时会产生开销 评估的单独线程,并且,如您的示例所示,可能会导致 在浪费的工作。有些表达式可能太小而无法进行并行评估 值得的开销。据我了解,即将出现一个完全准确的指标 表达式的成本等于解决停止问题,所以 你被降级为使用启发式方法来确定要做什么 并行评估。

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

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

答案 2 :(得分:7)

简短的回答:有时并行运行的东西变慢,而不是更快。并且弄清楚它何时以及何时不是一个好主意是一个尚未解决的研究问题。

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

答案 3 :(得分: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将以错误终止,而休息将有效。

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没有使用绿色线程(具有固定数量的内核线程 - 留出一些例外)为火花生成新线程,您仍然需要将数据从一个核心移动到另一个核心并在它们之间进行同步,这可能非常昂贵。 / p>

然而,Haskell确实引导了并行化而没有通过par和类似函数破坏语言的纯度。

答案 4 :(得分:2)

实际上,由于核心的可用数量较少,因此存在此类尝试但未在通用硬件上进行。该项目名为Reduceron。它以高水平的并行性运行Haskell代码。如果它被发布为proper 2 GHz ASIC core,我们将在Haskell执行速度方面取得重大突破。