Haskell中的多核编程 - Control.Parallel

时间:2009-11-25 18:26:46

标签: multithreading haskell parallel-processing multicore

我正在尝试学习如何使用Control.Parallel模块,但我认为我做得不对。

我正在尝试运行以下代码( fibs.hs )。

import Control.Parallel

fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = p `par` (q `pseq`  (p + q))
    where
      p = fib (n-1)
      q = fib (n-2)


main = print $ fib 30

我编译了这个:

ghc -O2 --make -threaded fibs.hs

然后我得到以下结果执行此程序(输出每个程序100次的Python脚本并返回执行时间的平均值和标准差):

./fibs +RTS -N1 -> avg= 0.060203 s, deviation = 0.004112 s  
./fibs +RTS -N2 -> avg= 0.052335 s, deviation = 0.006713 s  
./fibs +RTS -N3 -> avg= 0.052935 s, deviation = 0.006183 s  
./fibs +RTS -N4 -> avg= 0.053976 s, deviation = 0.007106 s  
./fibs +RTS -N5 -> avg= 0.055227 s, deviation = 0.008598 s  
./fibs +RTS -N6 -> avg= 0.055703 s, deviation = 0.006537 s  
./fibs +RTS -N7 -> avg= 0.058327 s, deviation = 0.007526 s  

我的问题是:

  1. 评估时究竟发生了什么:

    a `par` (b `pseq` (a + b))   ?
    

    我知道par b应该提示编译器有关与b并行计算a并返回b。好。但是pseq做了什么?

  2. 为什么我看到这么小的性能提升? 我在英特尔Core 2 Quad机器上运行它。我希望用-N5或-N6运行不会对性能产生真正的影响,或者程序实际上会开始表现得非常糟糕。但为什么我看不到从-N2到-N3的改善,为什么最初的改进如此之小?

4 个答案:

答案 0 :(得分:15)

作为Don explained,问题在于你创造了太多的火花。以下是您可以重写它以获得良好加速的方法。

import Control.Parallel

cutoff :: Int
cutoff = 20

parFib :: Int -> Int
parFib n | n < cutoff = fib n
parFib n = p `par` q `pseq` (p + q)
    where
      p = parFib $ n - 1
      q = parFib $ n - 2

fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

main :: IO ()
main = print $ parFib 40

示范:

[computer ~]$ ghc --make -threaded -O2 Main.hs
[1 of 1] Compiling Main             ( Main.hs, Main.o )
Linking Main ...
[computer ~]$ time ./Main +RTS -N1
102334155

real    0m1.509s
user    0m1.450s
sys     0m0.003s
[computer ~]$ time ./Main +RTS -N2
102334155

real    0m0.776s
user    0m1.487s
sys     0m0.023s
[computer ~]$ time ./Main +RTS -N3
102334155

real    0m0.564s
user    0m1.487s
sys     0m0.030s
[computer ~]$ time ./Main +RTS -N4
102334155

real    0m0.510s
user    0m1.587s
sys     0m0.047s
[computer ~]$ 

答案 1 :(得分:12)

你正在创建一个指数的火花(想想你在这里创建了多少个递归调用)。要实际获得良好的并行性,在这种情况下需要创建 less 并行工作,因为您的硬件无法处理那么多线程(因此GHC不会创建它们)。

解决方案是使用截止策略,如本演讲中所述:http://donsbot.wordpress.com/2009/09/05/defun-2009-multicore-programming-in-haskell-now/

基本上,一旦达到一定深度,切换到直线版本,并使用+ RTS -sstderr查看正在转换的火花数量,这样您就可以确定是否在浪费工作。

答案 2 :(得分:3)

由于没有人就pseq给出明确的答案,这里是official description

  

语义上与seq相同,但是   具有微妙的运营差异:   seq在两个论点中都是严格的,   所以编译器可能会,例如,   将seq b重新排列为b seq a   seq b。这通常没问题   当使用seq来表达严谨性时,   但它可能是一个问题   注释并行代码,   因为我们需要更多的控制权   评估顺序;我们可能想要   评估a之前的b,因为我们知道   那个b已经被激发了   与par。平行。

     

这就是为什么我们有pseq。相反   对于seq,pseq只是严格的   第一个参数(就编译器而言)   有关),这限制了   编译器可以进行的转换   做,并确保用户可以   保持对评估的控制   顺序。

答案 3 :(得分:1)

Re(1):par允许在另一个线程中计算a。我猜这里,但我认为pseq的行为很像seq:它强制首先计算第一个结果(好吧,seq不能保证这样做,但是在GHC的实践中确实如此)。因此,在这种情况下,a的计算分为一个线程,另一个线程计算b,然后对ab求和。

Re(2):这是一个非常简单的计算,分叉到其他线程;它可能与cpu一样快速地计算它本身。我认为线程的开销几乎和帮助这个简单的计算一样多。