我有一个简单的例程,需要一个向量的产品Double
。我试图并行化这些代码,但许多火花最终失败。这是也提供as a gist独立的基准:GHC Sparks为什么会失败?
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MagicHash #-}
{-# OPTIONS_GHC -O2 -Wall -threaded -fforce-recomp #-}
import Criterion.Main
import Control.Monad (when)
import Control.Parallel.Strategies (runEval,rpar,rseq)
import qualified Data.Vector.Primitive as PV
main :: IO()
main = do
let expected = PV.product numbers
when (not (serialProduct numbers == expected)) $ do
fail "serialProduct implementation incorrect"
defaultMain
[ bgroup "product"
[ bench "serial" $ whnf serialProduct numbers
, bench "parallel" $ whnf parallelProduct numbers
]
]
numbers :: PV.Vector Double
numbers = PV.replicate 10000000 1.00000001
{-# NOINLINE numbers #-}
serialProduct :: PV.Vector Double -> Double
serialProduct v =
let !len = PV.length v
go :: Double -> Int -> Double
go !d !ix = if ix < len then go (d * PV.unsafeIndex v ix) (ix + 1) else d
in go 1.0 0
-- | This only works when the vector length is a multiple of 8.
parallelProduct :: PV.Vector Double -> Double
parallelProduct v = runEval $ do
let chunk = div (PV.length v) 8
p2 <- rpar (serialProduct (PV.slice (chunk * 6) chunk v))
p3 <- rpar (serialProduct (PV.slice (chunk * 7) chunk v))
p1 <- rseq (serialProduct (PV.slice (chunk * 0) (chunk * 6) v))
return (p1 * p2 * p3)
这可以建立与运行:
ghc -threaded parallel_compute.hs
./parallel_compute +RTS -N4 -s
我有一个八芯盒,所以给人运行四个能力应没事的。该基准测试的结果是不是超级重要,但在这里,他们是:现在
benchmarking product/serial
time 11.40 ms (11.30 ms .. 11.53 ms)
0.999 R² (0.998 R² .. 1.000 R²)
mean 11.43 ms (11.37 ms .. 11.50 ms)
std dev 167.2 μs (120.4 μs .. 210.1 μs)
benchmarking product/parallel
time 10.03 ms (9.949 ms .. 10.15 ms)
0.999 R² (0.999 R² .. 1.000 R²)
mean 10.17 ms (10.11 ms .. 10.31 ms)
std dev 235.7 μs (133.4 μs .. 426.2 μs)
,运行时统计信息。这是我很困惑的地方:
124,508,840 bytes allocated in the heap
529,843,176 bytes copied during GC
80,232,008 bytes maximum residency (8344 sample(s))
901,272 bytes maximum slop
83 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 19 colls, 19 par 0.008s 0.001s 0.0001s 0.0003s
Gen 1 8344 colls, 8343 par 2.916s 1.388s 0.0002s 0.0008s
Parallel GC work balance: 76.45% (serial 0%, perfect 100%)
TASKS: 13 (1 bound, 12 peak workers (12 total), using -N4)
SPARKS: 1024 (502 converted, 0 overflowed, 0 dud, 28 GC'd, 494 fizzled)
INIT time 0.000s ( 0.002s elapsed)
MUT time 11.480s (10.414s elapsed)
GC time 2.924s ( 1.389s elapsed)
EXIT time 0.004s ( 0.005s elapsed)
Total time 14.408s (11.811s elapsed)
Alloc rate 10,845,717 bytes per MUT second
Productivity 79.7% of total user, 88.2% of total elapsed
在处理火花的部分,我们可以看到其中大约一半失败。这对我来说似乎令人难以置信。在parallelProduct
中,我们的主线程工作的任务比任何一个火花都大6倍。然而,似乎这些火花之一总是失败(或GCed)。这也不是一件小事。我们正在讨论一个需要几毫秒的计算,所以主线程可能在其他的thunk被触发之前完成它似乎是不可信的。
我的理解(这可能是完全错误的)是这种计算对于并发运行时应该是理想的。垃圾收集似乎是GHC中并发应用程序的最大问题,但我在这里执行的任务并没有产生任何垃圾,因为GHC将所有内容都拆箱的serialProduct
的内部变成了一个紧密的循环。
好的方面,我们做看到在基准测试中的并行版本加快11%。因此,成功引发的第八部分工作确实产生了可衡量的影响。我只是想知道为什么其他火花不能像我期望的那样工作。
任何帮助理解这一点,将不胜感激。
编辑
我已经更新the gist包括另一种实现方式:
-- | This only works when the vector length is a multiple of 4.
parallelProductFork :: PV.Vector Double -> Double
parallelProductFork v = unsafePerformIO $ do
let chunk = div (PV.length v) 4
var <- newEmptyMVar
_ <- forkIO $ evaluate (serialProduct (PV.slice (chunk * 0) chunk v)) >>= putMVar var
_ <- forkIO $ evaluate (serialProduct (PV.slice (chunk * 1) chunk v)) >>= putMVar var
_ <- forkIO $ evaluate (serialProduct (PV.slice (chunk * 2) chunk v)) >>= putMVar var
_ <- forkIO $ evaluate (serialProduct (PV.slice (chunk * 3) chunk v)) >>= putMVar var
a <- takeMVar var
b <- takeMVar var
c <- takeMVar var
d <- takeMVar var
return (a * b * c * d)
这其中有优异的性能:
benchmarking product/parallel mvar
time 3.814 ms (3.669 ms .. 3.946 ms)
0.986 R² (0.977 R² .. 0.992 R²)
mean 3.818 ms (3.708 ms .. 3.964 ms)
std dev 385.6 μs (317.1 μs .. 439.8 μs)
variance introduced by outliers: 64% (severely inflated)
但是,它倒在传统的并发原语,而不是使用火花。我不喜欢这个解决方案,但我提供它作为一个证据,证明它应该有可能通过基于火花的方法实现相同的性能。
没有什么会跳出来,但是如果您将'-A200M'和'-qa'中的一个/两个都添加到您的RTS选项,会发生什么情况? – jberryman
使用'./parallel_compute + RTS -N4 -s -qa -A200M',结果几乎相同。这是我所怀疑的,因为该计划没有真正分配太多。 –
嗯,我想那些统计数据来自'main'标准,并且可能不是非常有用(例如,标准计算上次并行计算的统计数据)。从经验来看,我不相信运行时的方式以及GC与并行/并发与默认设置交互的方式。一个观察:做一些快速的数学,你的序列版本就像我们希望的那样快。看起来可能的是你并行评估的这两个小块很容易被缓存行为损坏,但是我不能让这些数字加起来 – jberryman