Nei*_*ell 7 concurrency performance haskell
我正在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
Run Code Online (Sandbox Code Playgroud)
这会产生 20K 个线程,计算有多少线程使用IORef,当它们全部启动时,完成。使用该命令在 Windows 上的 GHC 8.10.1 上运行时ghc --make -O2 Main -threaded && main +RTS -N4,性能差异很大。有时需要> 1s(例如1.19s),有时需要< 0.1s(例如0.08s)。似乎它在大约 1/6 的时间中处于更快的桶中。为什么性能有差异?是什么让它跑得更快?
当我n放大到 1M 时,效果就会消失,它总是在 5+s 范围内。
我也可以在 Ubuntu 上确认相同的行为。除非我设置n=1M此行为不会消失,并且运行时间范围为 2 到 7 秒。
我相信调度程序的非确定性是运行时出现如此重大差异的原因。当然,这不是一个明确的答案,因为这只是我的猜测。
atomicModifyIORef'是用 CAS(比较和交换)实现的,因此根据线程的执行方式,函数old + 1将被重新计算或多或少的次数。换句话说,如果线程 B 在count线程 A 有机会更新 ref 之前更新 ref count,但在开始更新之后,它将必须从头开始更新操作,从而从 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
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
201 次 |
| 最近记录: |