在调查一些图书馆中一些奇怪的基准测试结果,我偶然发现了一些我不明白的行为,虽然这可能是很明显的。看来,许多操作(创建新的MutableArray,读取或修改IORef)所需的时间与内存中的数组数成比例增加。
这里是第一个例子:
- module Main
- where
- import Control.Monad
- import qualified Data.Primitive as P
- import Control.Concurrent
- import Data.IORef
- import Criterion.Main
- import Control.Monad.Primitive(PrimState)
- main = do
- let n = 100000
- allTheArrays <- newIORef []
- defaultMain $
- [ bench "array creation" $ do
- newArr <- P.newArray 64 () :: IO (P.MutableArray (PrimState IO) ())
- atomicModifyIORef' allTheArrays (\l-> (newArr:l,()))
- ]
我们正在创建一个新的数组并将其添加到堆栈。作为标准,更多的样本和堆栈增长,数组创建需要更多的时间,这似乎是线性和规律地增长:
更奇怪的是,IORef的读写操作受到影响,我们可以看到atomicModifyIORef’更快的估计是更多的数组是GC’d。
- main = do
- let n = 1000000
- arrs <- replicateM (n) $ (P.newArray 64 () :: IO (P.MutableArray (PrimState IO) ()))
- -- print $ length arrs -- THIS WORKS TO MAKE THINGS FASTER
- arrsRef <- newIORef arrs
- defaultMain $
- [ bench "atomic-mods of IORef" $
- -- nfIO $ -- OR THIS ALSO WORKS
- replicateM 1000 $
- atomicModifyIORef' arrsRef (\(a:as)-> (as,()))
- ]
评论的两行之一摆脱了这种行为,但我不知道为什么(也许在我们强制的列表的脊柱之后,元素实际上可以通过收集)。
问题
>这里发生了什么?
>它是预期的行为吗?
>有没有办法,我可以避免这种放缓?
编辑:我认为这与GC有关,需要更长的时间,但我想更准确地了解发生了什么,特别是在第一个基准。
奖金示例
最后,这里有一个简单的测试程序,可以用来预分配一些数组和时间一堆atomicModifyIORefs。这似乎表现出慢IORef行为。
- import Control.Monad
- import System.Environment
- import qualified Data.Primitive as P
- import Control.Concurrent
- import Control.Concurrent.Chan
- import Control.Concurrent.MVar
- import Data.IORef
- import Criterion.Main
- import Control.Exception(evaluate)
- import Control.Monad.Primitive(PrimState)
- import qualified Data.Array.IO as IO
- import qualified Data.Vector.Mutable as V
- import System.cpuTime
- import System.Mem(performGC)
- import System.Environment
- main :: IO ()
- main = do
- [n] <- fmap (map read) getArgs
- arrs <- replicateM (n) $ (P.newArray 64 () :: IO (P.MutableArray (PrimState IO) ()))
- arrsRef <- newIORef arrs
- t0 <- getcpuTimeDouble
- cnt <- newIORef (0::Int)
- replicateM_ 1000000 $
- (atomicModifyIORef' cnt (\n-> (n+1,())) >>= evaluate)
- t1 <- getcpuTimeDouble
- -- make sure these stick around
- readioRef cnt >>= print
- readioRef arrsRef >>= (flip P.readArray 0 . head) >>= print
- putStrLn "The time:"
- print (t1 - t0)
带有-hy的堆配置文件主要显示为MUT_ARR_PTRS_CLEAN,我不完全理解。
如果你想重现,这里是我一直在使用的cabal文件
- name: small-concurrency-benchmarks
- version: 0.1.0.0
- build-type: Simple
- cabal-version: >=1.10
- executable small-concurrency-benchmarks
- main-is: Main.hs
- build-depends: base >=4.6,criterion,primitive
- default-language: Haskell2010
- ghc-options: -O2 -rtsopts
编辑:这里是另一个测试程序,可以用于比较减缓与相同大小的阵列vs [整数]的堆。它需要一些试验和错误调整n和观察分析,以获得类似的运行。
有趣的是,当我测试这个,我发现相同的堆大小的数组的数组影响性能的程度大于[Integer]。例如。
- Baseline 20M 200M
- Lists: 0.7 1.0 4.4
- Arrays: 0.7 2.6 20.4
结论(WIP)
>这很可能是由于GC行为
>但是可变的unBoxed数组似乎导致更多的服务器减速(见上文)。设置RTS -A200M使数组垃圾版本的性能符合列表版本,支持这与GC有关。
>减速与分配的阵列数量成比例,而不是阵列中总单元数。这里是一组运行,显示,对于类似的测试,main4,分配的数量分配的时间分配的时间和一个完全不相关的“有效载荷”的影响。这是为16777216总细胞(分为许多阵列):
- Array size Array create time Time for "payload":
- 8 3.164 14.264
- 16 1.532 9.008
- 32 1.208 6.668
- 64 0.644 3.78
- 128 0.528 2.052
- 256 0.444 3.08
- 512 0.336 4.648
- 1024 0.356 0.652
并且在16777216 * 4单元上运行相同的测试,显示与上面基本相同的有效负载时间,仅向下移动两个位置。
>从我理解GHC的工作原理,看看(3),我认为这种开销可能只是指向所有这些数组贴在周围的remembered set(参见:here),以及任何开销导致GC。