[英]Unexpectedly poor and weirdly bimodal performance for store loop on Intel Skylake
[英]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.