2017-10-05 145 views
4

我有一個簡單的例程,需要一個向量的產品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) 

但是,它倒在傳統的併發原語,而不是使用火花。我不喜歡這個解決方案,但我提供它作爲一個證據,證明它應該有可能通過基於火花的方法實現相同的性能。

+0

沒有什麼會跳出來,但是如果您將'-A200M'和'-qa'中的一個/兩個都添加到您的RTS選項,會發生什麼情況? – jberryman

+0

使用'./parallel_compute + RTS -N4 -s -qa -A200M',結果幾乎相同。這是我所懷疑的,因爲該計劃沒有真正分配太多。 –

+0

嗯,我想那些統計數據來自'main'標準,並且可能不是非常有用(例如,標準計算上次並行計算的統計數據)。從經驗來看,我不相信運行時的方式以及GC與並行/併發與默認設置交互的方式。一個觀察:做一些快速的數學,你的序列版本就像我們希望的那樣快。看起來可能的是你並行評估的這兩個小塊很容易被緩存行爲損壞,但是我不能讓這些數字加起來 – jberryman

回答

4

這裏的問題是創建火花不會立即喚醒閒置功能,請參閱here。默認情況下,調度時間間隔爲20ms,因此當您創建一個spark時,最多需要20 ms才能將其轉換爲實際線程。那時調用線程很可能已經評估了thunk,並且火花將會被GC'd或失敗。

相比之下,forkIO將立即喚醒閒置能力,如果有的話。這就是爲什麼顯式併發比並行策略更可靠。

您可以使用-C選項(docs)減少調度間隔來解決問題。例如。 +RTS -C0.01似乎夠用了。

+0

這解決了它。雖然調整這樣的調度間隔似乎可能會對其他事情產生負面影響。所以我認爲,在正常情況下,如果你要引發事情,你應該確保所有火花加上主線程完成的工作需要超過20毫秒。否則,幾乎所有的東西都會失效,除非調度即將到來。我一直想知道應該如何細化火花的門檻,我的理解是,現在大致就是這樣。另外,你在哪裏找到'-C的文檔?我找不到它。 –

+0

@AndrewThaddeusMartin我添加了一個鏈接到文檔。重新設定門檻 - 是的,這也是我的印象。儘管我在實踐中從未使用過並行策略。 – Yuras

+0

@AndrewThaddeusMartin可能是有道理的提出一個錯誤來看看ghc開發人員對火花調度的看法。我想知道是否可以立即喚醒一個能力。 – Yuras

相關問題