为什么Haskell中没有隐含的并行性?
Haskell是function性和纯粹的,所以基本上它具有编译器能够处理隐式并行所需的所有属性。
考虑这个微不足道的例子:
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实现这样的function呢? 什么是实际的原因?
这是一个长期研究的话题。 虽然你可以在Haskell代码中隐含地获得并行性,但问题在于对于当前的硬件来说,太多的并行性太好了。
所以你最终花费在记账上,而不是更快地运行。
由于我们没有无限的并行硬件,所以只能select正确的粒度 – 太粗糙,并且会有空闲的处理器,而且费用太高,而且开销也是不可接受的。
我们所拥有的是更粗粒度的并行(火花),适合于产生数千或数百万个并行任务(所以不在指令级别),它们映射到我们今天通常可用的less数核心上。
请注意,对于某些子集(如arrays处理),存在具有严格成本模型的全自动并行库。
有关此背景信息,请参阅en-us/um/people/tharris/papers/2007-fdip.html ,其中介绍了在任意Haskell程序中插入par
的自动方法。
虽然你的代码块可能不是最好的例子,由于a
和b
之间的隐式数据依赖,值得注意的是,这两个绑定通勤的
f = do a <- Just 1 b <- Just $ Just 2 ...
会给出相同的结果
f = do b <- Just $ Just 2 a <- Just 1 ...
所以这仍然可以以推测的方式并行化。 值得注意的是,这并不需要与单子有任何关系。 例如,我们可以并行评估一个let
block中的所有独立expression式,或者我们可以引入let
一个版本。 Common Lisp的并行库执行此操作。
现在,我绝不是这方面的专家,但这是我对这个问题的理解。 一个主要的绊脚石是决定何时平行多个expression式的评估是有利的。 与开始单独的线程进行评估相关的开销,如您的示例所示,可能会导致浪费的工作。 有些expression式可能太小,不足以进行并行评估。 根据我的理解,提出一个expression式的成本的完全准确度量将等于解决暂停问题,因此您将被降级为使用启发式方法来确定要并行评估的内容。
那么在一个问题上抛出更多内核并不总是更快。 即使明确并行处理有许多Haskell库的问题,由于大量的内存分配和使用以及垃圾收集器和CPUcaching的负担,通常不会仅仅通过并行计算expression式就能看到更多的加速。 你最终需要一个很好的紧凑的内存布局,并智能地遍历你的数据。 有16个线程遍历链表将只是在你的内存总线瓶颈,实际上可以让事情变得更慢。
至less,什么expression式可以被有效地并行化,对许多程序员来说是不明显的(至less对于这个程序员来说)是不明显的,所以让一个编译器有效地做到这一点并不重要。
简短的回答:有时候并行运行的东西会变慢,而不是变快。 弄清楚它是什么时候,什么时候不是一个好主意是一个未解决的研究问题。
但是,你仍然可以“突然利用所有这些核心,而不用打扰线程,死锁和竞赛条件”。 这不是自动的; 你只需要给编译器一些提示就可以了! 😀
其中一个原因是因为Haskell是非严格的,它不会默认评估任何东西。 一般来说,编译器不知道a
和b
计算会终止,因此试图计算它会浪费资源:
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)
考虑以下function
main1 x = case x of Just _ -> putStrLn "Just" Nothing -> putStrLn "Nothing"
(a, b)
部分不需要评估。 只要你得到那个x =只是_你可以继续分支 – 因此它将适用于所有值,但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"
此function将首先打印第一个列表,然后第二个。 它将适用于z
(导致打印无限的数字stream,但Haskell可以处理它)。 b
最终会耗尽内存。
现在一般来说,你不知道计算是否终止,它将消耗多less资源。 Haskell中的无限列表完全正确:
main = maybe (return ()) (print . take 5 . snd) b -- Prints first 5 Fibbonacci numbers
因此,在Haskell中产生线程来评估expression式可能会尝试评估一些并不意味着要被完全评估的东西 – 比如说所有素数的列表 – 然而程序员使用它作为结构的一部分。 上面的例子非常简单,你可能会争辩说,编译器可以注意到它们 – 但是由于Halting问题(不能编写任意程序和input并检查它是否终止)的程序是不可能的 – 因此它不是安全的优化。
另外 – 这是其他答案提到 – 很难预测额外线程的开销是否值得参与。 即使GHC没有为使用绿色线程的火花产生新的线程(固定数量的内核线程 – 抛开一些例外),您仍然需要将数据从一个内核移动到另一个内核,并在它们之间同步,这可能是相当昂贵的。
然而,Haskell确实已经引导了并行化,而没有通过类似的function来打破语言的纯粹性。
实际上,由于可用的核心数量很less,所以这种尝试却没有在通用硬件上进行。 该项目被称为Reduceron 。 它以高水平的并行性运行Haskell代码。 如果它是以合适的2 GHz ASIC内核发布的话,那么Haskell的执行速度就会有一个严重的突破。