如何在Haskell中为软实时应用程序优化垃圾收集?

时间:2015-11-05 13:43:43

标签: haskell optimization garbage-collection

我在Haskell编写了一个软实时应用程序,它处理模拟物理,碰撞检测,所有这些好东西。在做所有这些事情的时候,我分配了大量的内存,如果我愿意的话,我可能会优化我的内存使用量,但是由于我能够很好地使用40%的CPU并且只使用1%的RAM,所以并没有这样做。似乎有必要。我所看到的是,很多时候,当垃圾收集器启动时,帧被跳过。通过使用threadscope进行分析,我已经验证了这是问题的原因:当垃圾收集器完成其业务时,有时不会有0.05秒的有用计算,导致最多3个跳帧,是非常明显的,非常讨厌。

现在,我尝试通过每帧手动调用performMinorGC来解决这个问题,这似乎可以缓解这个问题,使其更加顺畅,除了整体CPU使用率急剧上升到70%左右。显然,我宁愿避免这种情况。

我尝试的另一件事是使用-H64k将GC的分配空间从512k减少到64k,我还尝试设置-I0.03以尝试让它更频繁地收集。这两个选项都改变了我在threadscope中看到的垃圾收集模式,但它们仍然导致跳过帧。

任何有GC优化经验的人都可以帮助我吗?我注定要手动调用performMinorGC并忍受导致的巨大性能损失吗?

修改

我尝试在这些测试中运行它的时间相似,但由于它是实时的,所以没有任何意义,它已经完成了#39;

每4帧performMinorGC运行时统计信息:

     9,776,109,768 bytes allocated in the heap
     349,349,800 bytes copied during GC
      53,547,152 bytes maximum residency (14 sample(s))
      12,123,104 bytes maximum slop
             105 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0     15536 colls, 15536 par    3.033s   0.997s     0.0001s    0.0192s
  Gen  1        14 colls,    13 par    0.207s   0.128s     0.0092s    0.0305s

  Parallel GC work balance: 6.15% (serial 0%, perfect 100%)

  TASKS: 20 (2 bound, 13 peak workers (18 total), using -N4)

  SPARKS: 74772 (20785 converted, 0 overflowed, 0 dud, 38422 GC'd, 15565 fizzled)

  INIT    time    0.000s  (  0.001s elapsed)
  MUT     time    9.773s  (  7.368s elapsed)
  GC      time    3.240s  (  1.126s elapsed)
  EXIT    time    0.003s  (  0.004s elapsed)
  Total   time   13.040s  (  8.499s elapsed)

  Alloc rate    1,000,283,400 bytes per MUT second

  Productivity  75.2% of total user, 115.3% of total elapsed

gc_alloc_block_sync: 29843
whitehole_spin: 0
gen[0].sync: 11
gen[1].sync: 71

没有performMinorGC

  12,316,488,144 bytes allocated in the heap
     447,495,936 bytes copied during GC
      63,556,272 bytes maximum residency (15 sample(s))
      15,418,296 bytes maximum slop
             146 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0     19292 colls, 19292 par    2.613s   0.950s     0.0000s    0.0161s
  Gen  1        15 colls,    14 par    0.237s   0.165s     0.0110s    0.0499s

  Parallel GC work balance: 2.67% (serial 0%, perfect 100%)

  TASKS: 17 (2 bound, 13 peak workers (15 total), using -N4)

  SPARKS: 100714 (29688 converted, 0 overflowed, 0 dud, 47577 GC'd, 23449 fizzled)

  INIT    time    0.000s  (  0.001s elapsed)
  MUT     time   13.377s  (  9.917s elapsed)
  GC      time    2.850s  (  1.115s elapsed)
  EXIT    time    0.000s  (  0.006s elapsed)
  Total   time   16.247s  ( 11.039s elapsed)

  Alloc rate    920,744,995 bytes per MUT second

  Productivity  82.5% of total user, 121.4% of total elapsed

gc_alloc_block_sync: 68533
whitehole_spin: 0
gen[0].sync: 9
gen[1].sync: 147

现在没有performMinorGC的总体生产率似乎低于我昨天因某种原因进行测试时 - 在它始终> 90%之前。

1 个答案:

答案 0 :(得分:4)

你有不同的老一代。它就像100Mb一样大。

默认情况下GHC在最后一个主要GC之后堆大小达到其大小的2倍时执行主要GC。这意味着在某些时候GC必须扫描并复制50Mb的数据。如果您的处理器具有10Gb内存吞吐量限制,则加载和复制50Mb将至少需要0.01秒(与gen1平均和最大暂停相比。)

(我假设您检查了事件日志以确保主要GC在0.05秒暂停期间实际工作。所以当GC等待其他线程而不是真正的工作时,这不是线程同步的问题。)< / p>

因此,为了最大限度地减少GC暂停,您应该确保老一代很小。如果这个50Mb中的大部分是一开始就分配的静态数据,直到结束(例如纹理或网格),那么你就是stuck。我所知道的唯一解决方法是将数据打包到例如可存储的矢量,并在需要时再次打开它的部分。

如果数据是在执行期间分配的,并且生活时间有限(但足以在几代人中生存下来),那么请尝试重新考虑您的管道。通常没有数据应该存活一帧,所以你做错了。例如。你不应该保留数据。

其他不良信号 - gen0 max暂停0.02秒。这很奇怪。默认情况下,gen0分配区域为0.5Mb,因此gen0 GC应该很快。可能你有大remembered set。可能的原因:可变结构(IORef,可变矢量等)或大量懒惰的更新。

一个次要(可能是无关的)问题:看起来你正在使用隐式并行,但只有1/3的火花被转换。你分配了太多的spart,其中1/2是GC&#39; d。