簡體   English   中英

GHC forkIO 雙峰性能

[英]GHC forkIO bimodal performance

我正在使用以下代碼對forkIO進行基准測試:

import System.Time.Extra
import Control.Concurrent
import Control.Monad
import Data.IORef


n = 200000

main :: IO ()
main = do
    bar <- newEmptyMVar
    count <- newIORef (0 :: Int)
    (d, _) <- duration $ do
        replicateM_ n $ do
            forkIO $ do
                v <- atomicModifyIORef' count $ \old -> (old + 1, old + 1)
                when (v == n) $ putMVar bar ()
        takeMVar bar
    putStrLn $ showDuration d

這會產生 20K 線程,計算有多少使用IORef運行,當它們全部啟動時,完成。 當使用命令ghc --make -O2 Main -threaded && main +RTS -N4在 Windows 上的 GHC 8.10.1 上運行時,性能差異很大。 有時需要 > 1 秒(例如 1.19 秒),有時需要 < 0.1 秒(例如 0.08 秒)。 似乎它大約有 1/6 的時間在更快的桶中。 為什么性能差異? 是什么導致它更快地 go?

當我將n放大到 1M 時,效果就會消失,並且始終在 5+s 范圍內。

我也可以在 Ubuntu 上確認相同的行為。 除非我設置n=1M ,否則此行為不會遠離 go 並且我的運行時間范圍為 2 到 7 秒。

我相信調度程序的不確定性是導致運行時出現如此顯着差異的原因。 當然,這不是一個確定的答案,因為這只是我的猜測。

atomicModifyIORef'使用 CAS(比較和交換)實現,因此根據線程的執行方式,function old + 1將或多或少地重新計算。 換句話說,如果線程 B 在線程 A 有機會更新count ref 之前更新了count ref,但在它開始更新之后,它將不得不從頭開始更新操作,從而從ref 並再次重新計算old + 1

如果您運行main +RTS -N1 ,您會發現不僅運行程序所需的時間要少得多,而且執行之間的運行時間也非常一致。 我懷疑這是因為只有一個線程可以在任何時候運行,並且在atomicModifyIORef'完成之前沒有搶占。

希望對 Haskell RTS 有深入了解的其他人可以提供對這種行為的更多見解,但這是我的看法。

編輯

@NeilMitchel 評論道:

我根本不相信這與原子修改有關

為了證明 IORef 確實存在錯誤,這里有一個使用PVar的實現,它依賴於下面的casIntArray# 它不僅快 10 倍,而且沒有觀察到差異:

import System.Time.Extra
import Control.Concurrent
import Control.Monad
import Data.Primitive.PVar -- from `pvar` package


n = 1000000

main :: IO ()
main = do
    bar <- newEmptyMVar
    count <- newPVar (0 :: Int)
    (d, _) <- duration $ do
        replicateM_ n $ do
            forkIO $ do
                v <- atomicModifyIntPVar count $ \old -> (old + 1, old + 1)
                when (v == n) $ putMVar bar ()
        takeMVar bar
    putStrLn $ showDuration d

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM