gpt4 book ai didi

haskell - 为什么 GHC Sparks 会失败?

转载 作者:行者123 更新时间:2023-12-04 14:55:00 29 4
gpt4 key购买 nike

我有一个简单的例程,它采用 Double 的向量的乘积.我正在尝试并行化此代码,但许多 Spark 最终都失败了。这是一个独立的基准,也提供了 as a gist :

{-# 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

在处理 Spark 的部分中,我们可以看到大约一半的 Spark 消失了。这对我来说似乎令人难以置信。在 parallelProduct ,我们让主线程处理的任务是给任何一个 Spark 的任务的 6 倍。然而,似乎这些 Spark 之一总是失败(或 GCed)。这也不是一项小工作。我们正在谈论一个需要几毫秒的计算,因此主线程可以在其他 thunk 被触发之前完成它似乎是难以置信的。

我的理解(可能完全错误)是这种计算应该是并发运行时的理想选择。垃圾收集似乎是 GHC 中并发应用程序的最大问题,但我在这里所做的任务几乎不会产生任何垃圾,因为 GHC 变成了 serialProduct 的内部。进入一个紧密的循环,所有东西都没有装箱。

从好的方面来说,我们 在基准测试中看到并行版本的 11% 加速。因此,成功激发的第八部分工作确实产生了可衡量的影响。我只是想知道为什么其他 Spark 不像我期望的那样工作。

任何有关理解这一点的帮助将不胜感激。

编辑

我更新了 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)

但是,它依靠传统的并发原语而不是使用 Spark 。我不喜欢这个解决方案,但我提供它作为证据,证明使用基于 Spark 的方法应该可以实现相同的性能。

最佳答案

这里的问题是创建 spark 不会立即唤醒空闲功能,请参阅 here .默认调度间隔是 20ms,所以当你创建一个 spark 时,最多需要 20ms 才能把它变成一个真正的线程。到那时,调用线程很可能已经评估了 thunk,并且 spark 将被 GC 或 fizzled。

相比之下,forkIO将立即唤醒空闲功能(如果有)。这就是为什么显式并发比并行策略更可靠的原因。

您可以通过使用 -C 减少调度间隔来解决此问题。选项(docs)。例如。 +RTS -C0.01似乎已经足够了。

关于haskell - 为什么 GHC Sparks 会失败?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46586941/

29 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com