我发现了一个似乎包含内存泄漏的库的一小部分。下面的代码尽可能小,但仍然产生与实际代码相同的结果。
import System.Random
import Control.Monad.State
import Control.Monad.Loops
import Control.DeepSeq
import Data.Int (Int64)
import qualified Data.Vector.Unboxed as U
vecLen = 2048
main = flip evalStateT (mkStdGen 13) $ do
let k = 64
cs <- replicateM k transform
let sizeCs = k*2*7*vecLen*8 -- 64 samples, 2 elts per list, each of len 7*vecLen, 8 bytes per Int64
(force cs) `seq` lift $ putStr $ "Expected to use ~ " ++ (show ((fromIntegral sizeCs) / 1000000 :: Double)) ++ " MB of memory\n"
transform :: (Monad m, RandomGen g)
=> StateT g m [U.Vector Int64]
transform = do
e <- liftM ((U.map round) . (uncurry (U.++)) . U.unzip) $ U.replicateM (vecLen `div` 2) sample
c1 <- U.replicateM (7*vecLen) $ state random
return [U.concat $ replicate 7 e, c1]
sample :: (RandomGen g, Monad m) => StateT g m (Double, Double)
sample = do
let genUVs = liftM2 (,) (state $ randomR (-1,1)) (state $ randomR (-1,1))
-- memory usage drops and productivity increases to about 58% if I set the guard to "False" (the real code needs a guard here)
uvGuard (u,v) = u+v >= 2 -- False --
(u,v) <- iterateWhile uvGuard genUVs
return (u, v)
删除任何更多代码可显着提高内存使用/ GC,时间或两者的性能。但是,我需要计算上面的代码,所以真正的代码不能更简单。
例如,如果我使e和c1都从sample
获取值,则代码使用27 MB内存并在GC中花费9%的运行时间。如果我使e和c1都使用state random
,我使用大约400MB的内存,并且只在GC中花费32%的运行时间。
主要参数是vecLen
,我真正需要大约8192.为了加快分析,我使用vecLen=2048
生成了以下所有结果,但问题更严重,因为vecLen
增加了
使用
进行编译ghc test -rtsopts
我明白了:
> ./test +RTS -sstderr
Working...
Expected to use ~ 14.680064 MB of memory
Done
3,961,219,208 bytes allocated in the heap
2,409,953,720 bytes copied during GC
383,698,504 bytes maximum residency (17 sample(s))
3,214,456 bytes maximum slop
869 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 7002 colls, 0 par 1.33s 1.32s 0.0002s 0.0034s
Gen 1 17 colls, 0 par 1.60s 1.84s 0.1080s 0.5426s
INIT time 0.00s ( 0.00s elapsed)
MUT time 2.08s ( 2.12s elapsed)
GC time 2.93s ( 3.16s elapsed)
EXIT time 0.00s ( 0.03s elapsed)
Total time 5.01s ( 5.30s elapsed)
%GC time 58.5% (59.5% elapsed)
Alloc rate 1,904,312,376 bytes per MUT second
Productivity 41.5% of total user, 39.2% of total elapsed
real 0m5.306s
user 0m5.008s
sys 0m0.252s
使用-p或-h *进行性能分析并不会显示太多,至少对我而言。
然而,线程范围很有意思:
在我看来,我正在吹堆,所以GC正在发生,堆大小翻倍。实际上,当我使用-H4000M运行时,线程范围看起来稍微更均匀(工作量更少,双重GC),但我仍然花费大约60%的整个运行时间来执行GC。使用-O2进行编译会更糟糕,超过70%的运行时间用于GC。
问题: 1.为什么GC运行这么多? 2. 我的堆使用量是否意外大?如果是这样,为什么?
对于问题2,我意识到堆使用量可能超过我的“预期”内存使用量,即使是很多。但800MB似乎对我来说太过分了。 (这是我应该看的数字吗?)
答案 0 :(得分:5)
为了攻击这样的问题,我经常会先用SCC
编译语乱丢代码,无论我觉得哪里可能有大量的分配。在这种情况下,我怀疑e
中的c1
和transform
以及genUVs
中的sample
,
...
transform :: (Monad m, RandomGen g)
=> StateT g m [U.Vector Int64]
transform = do
e <- {-# SCC hi1 #-} liftM (U.map round . uncurry (U.++) . U.unzip) $ U.replicateM (vecLen `div` 2) sample
c1 <- {-# SCC hi2 #-} U.replicateM (7*vecLen) $ state random
return [U.concat $ replicate 7 e, c1]
sample :: (RandomGen g, Monad m) => StateT g m (Double, Double)
sample = do
let genUVs = {-# SCC genUVs #-} liftM2 (,) (state $ randomR (-1,1)) (state $ randomR (-1,1))
-- memory usage drops and productivity increases to about 58% if I set the guard to "False" (the real code needs a guard here)
uvGuard (u,v) = u+v >= 2 -- False --
(u,v) <- iterateWhile uvGuard genUVs
return $ (u, v)
我们首先查看-hy
以查看相关对象的类型。这揭示了许多不同类型,包括Integer
,Int32
,StdGen
,Int
和(,)
。使用-hc
,我们可以确定几乎所有这些值都是在c1
transform
中分配的。这由-hr
确认,它告诉我们谁拥有对这些对象的引用(从而防止它们被垃圾收集)。我们可以通过检查c1
保留的对象类型来进一步确认-hrc1 -hy
是罪魁祸首(假设我们已使用{-# SCC c1 #-}
注释它。)
c1
保留这么多对象这一事实表明它在我们希望它时没有被评估。在评估之后c1
是一个相当短的向量,在评估之前它需要几千个随机种子,相关的闭包,以及可能的其他一些对象。
Deepseq
c1
将GC时间从59%提高到23%,并将内存消耗降低一个数量级。这是return
中的终端transform
转入,
deepseq c1 $ return [U.concat $ replicate 7 e, c1]
在此之后,配置文件看起来相当合理,最大空间用户在ARR_WORDS
(按预期)分配的transform
大约为10MB,后跟一些元组,可能来自genUVs
。