编辑: 事实证明,通常情况下(不仅仅是数组/参考操作)会减慢创建的阵列数量,所以我猜这可能只是测量增加的GC时间和可能没有我想象的那么奇怪。但我真的很想知道(并学习如何找出)这里发生了什么,以及是否有某种方法可以在创建大量小数组的代码中缓解这种影响。原始问题如下。
在调查图书馆中一些奇怪的基准测试结果时,我偶然发现了一些我不理解的行为,尽管它可能非常明显。似乎许多操作所需的时间(创建新的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&#39; 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需要更长时间有关,但我想更准确地了解发生了什么,特别是在第一个基准测试中。
最后,这是一个简单的测试程序,可用于预分配一些数组并为一堆atomicModifyIORef
计时。这似乎表现出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
修改:这是另一个测试程序,可用于将减速与相同大小的数组与[Integer]
的堆进行比较。需要一些试验和错误调整n
并观察分析以获得可比较的运行。
main4 :: IO ()
main4= do
[n] <- fmap (map read) getArgs
let ns = [(1::Integer).. n]
arrsRef <- newIORef ns
print $ length ns
t0 <- getCPUTimeDouble
mapM (evaluate . sum) (tails [1.. 10000])
t1 <- getCPUTimeDouble
readIORef arrsRef >>= (print . sum)
print (t1 - t0)
有趣的是,当我测试它时,我发现相同堆大小的数组会比[Integer]
更大程度地影响性能。 E.g。
Baseline 20M 200M
Lists: 0.7 1.0 4.4
Arrays: 0.7 2.6 20.4
这很可能是由于GC行为
但可变的未装箱阵列似乎导致更严重的减速(见上文)。设置+RTS -A200M
会使阵列垃圾版本的性能与列表版本保持一致,从而支持这与GC有关。
减速与分配的数组数成正比,而不是数组中的总单元数。下面是一组运行,显示对于main4
的类似测试,分配的数量对分配所花费的时间和完全不相关的&#34;有效负载&#34;的影响。这是针对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中所有这些数组的指针(另请参阅:{ {3}}),以及导致GC的任何开销。
答案 0 :(得分:6)
每个可变数组的每个次要GC都会支付线性开销,这些数组会保持活动并升级到旧代。这是因为GHC无条件地将所有可变数组放在可变列表上,并遍历每个次要GC的整个列表。有关详细信息,请参阅https://ghc.haskell.org/trac/ghc/ticket/7662,以及我的邮件列表对您问题的回复:http://www.haskell.org/pipermail/glasgow-haskell-users/2014-May/024976.html
答案 1 :(得分:4)
我认为你肯定会看到GC的影响。我在cassava(https://github.com/tibbe/cassava/issues/49#issuecomment-34929984)中遇到了一个相关问题,其中GC时间随着堆大小的增加呈线性增长。
尝试在内存中保留越来越多的数组时测量GC时间和变异器时间的增加情况。
您可以使用+RTS
选项缩短GC时间。例如,尝试将-A
设置为L3缓存大小。