haskell - 过多的垃圾收集(和内存使用?)

标签 haskell memory-leaks profiling heap-memory monads

我发现了一个库的一小部分似乎包含内存泄漏。下面的代码尽可能小,但仍然产生与真实代码相同的结果。

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 都使用状态随机,我会使用大约 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* 进行分析并不能透露太多信息,至少对我来说是这样。

但是,线程作用域很有趣:threadscope

在我看来,我好像在破坏堆,所以 GC 正在发生,堆大小加倍。事实上,当我使用 -H4000M 运行时,threadscope 看起来稍微更均匀(更少的双倍工作,双倍 GC),但我仍然花费大约 60% 的总体运行时间来执行 GC。使用 -O2 编译情况更糟,超过 70% 的运行时间都花在 GC 上。

问题: 1. 为什么GC运行这么多? 2. 我的堆使用量是否出乎意料地大?如果是这样,为什么?

对于问题 2,我意识到堆使用量可能会超出我的“预期”内存使用量,甚至超出很多。但800MB对我来说似乎有点太大了。 (这就是我应该查看的数字吗?)

最佳答案

为了解决这样的问题,我通常会在我认为可能有大量分配的地方使用 SCC 编译指示来开始代码。在这种情况下,我怀疑 transform 中的 ec1 以及 sample< 中的 genUVs/,

...

transform :: (Monad m, RandomGen g)
           => StateT g m [U.Vector Int64]
transform = do
      e <- {-# SCC e #-} liftM (U.map round . uncurry (U.++) . U.unzip) $ U.replicateM (vecLen `div` 2) sample
      c1 <- {-# SCC 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 = {-# 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 查看相关对象的类型。这揭示了许多不同的类型,包括IntegerInt32StdGenInt(, )。使用-hc我们可以确定几乎所有这些值都分配在transformc1中。 -hr 证实了这一点,它告诉我们谁持有对这些对象的引用(从而防止它们被垃圾收集)。我们可以通过使用 -hrc1 -hy 检查它保留的对象类型来进一步确认 c1 是罪魁祸首(假设我们用 {-# 注解它) SCC c1 #-})。

事实上,c1 保留了如此多的对象,这表明它没有在我们希望的时候被评估。虽然在评估之后,c1 是一个相当短的向量,但在评估之前,它需要数千个随机种子、关联的闭包,以及可能的许多其他对象。

Deepseqing c1 使 GC 时间从 59% 减少到 23%,并将内存消耗减少一个数量级。这就是 transform 中的终端 return 转变成,

deepseq c1 $ return [U.concat $ replicate 7 e, c1]

此后,配置文件看起来相当合理,最大的空间用户在 transform 中分配了大约 10MB 的 ARR_WORDS(如预期),后面是一些元组,可能来自 genUVs.

关于haskell - 过多的垃圾收集(和内存使用?),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19164151/

相关文章:

haskell - 依赖类型的 ghc-7.6 类实例

haskell - 如何转换 a -> Float?

Haskell - 将字符串中指定位置的一个字符更改为另一个指定的字符

haskell - tryhaskell.org 似乎不支持 GHCi 命令

visual-studio - 如何分析 vs2008/10 中的特定代码块?

javascript - bootstrap 附加插件内存泄漏

iphone - NSMutableString appendString 的内存泄漏

objective-c - iOS 应用仅在某些随机设备上引发 0x8badf00d

ios - CoreAnimation 性能分析 - CAReplicatorLayer 和 CAShapeLayer

java - 针对特定包的 Scala/Java 分析