Re: [MLton] tuning up mlton gc

Matthew Fluet <[email protected]> Wed, 23 Jul 2014 10:54:54 -0400
Newsgroups gmane.comp.lang.ml.mlton.devel
Message-ID <CAMrhFL763nRZm=HPJzn87jqz_6Fwg5aS-9oTNUXPMs5vFmPiYQ@mail.gmail.com>
On Wed, Jul 23, 2014 at 10:03 AM, Matthew Fluet <[email protected]> wrote:
> ---------- Forwarded message ----------
> From: Bernard Berthomieu <[email protected]>
> Date: Mon, Jul 21, 2014 at 10:44 AM
> Subject: tuning up mlton gc
> To: Matthew Fluet <[email protected]>, Stephen Weeks <[email protected]>
>
> We are using mlton at our place to implement model-checking tools.
> We are quite happy with it, but the cost of garbage collection in our
> applications always bothered me. Our applications typically generate
> a lot of data, a large percentage of which is persistent (state spaces).
> I guess this is the kind of applications that would most benefit from
> generational garbage collection.

Agreed that generational garbage collection makes sense when there is
a large amount of data that is live over many garbage collections.
This is especially true for the copying collection, since the time for
a copying collection is proportional to the amount of live data.

> I observed that mlton does not start with generational gc, but rather
> with a simple two-space copying, possibly switching later to generational,
> upon some conditions. I could not find in the mlton sources a simple
> way to force generational garbage collection from the start, but I've
> found that some gc constants could be tuned by the user (though these
> options are not documented).
> After some experiments, I found that setting live-ratio to 4.0,
> rather than the default 8.0, consistently resulted in a significant
> speedup of my applications.

After each garbage collection, the decision about whether or not to
use a minor collection (i.e., to use generational garbage collection)
for the next garbage collection is computed by:
  https://github.com/MLton/mlton/blob/on-20130715-release/runtime/gc/gc_state.c#L71-L99

>From this, you can observe that the runtime system options that
directly control whether or not to use generational collection are:
 * nursery-ratio  (default: 10.0)
 * copy-generational-ratio  (default: 4.0)
 * mark-compact-generational-ratio  (default: 8.0)

Setting live-ratio to 4.0 indirectly forces generational garbage
collection because it tends to make the following true:
  (float)h->size / (float)s->lastMajorStatistics.bytesLive <=
s->controls.ratios.copyGenerational
This is because live-ratio controls the desired size of the heap
relative to the live data after a major garbage collection; that is,
after each major garbage collection, the heap is resized so as to
satisfy:
  h->size == bytesLive * s->controls.ratios.live
(subject to other constraints such as minimum, maximum, and/or fixed
heap size, RAM size, etc. and skipping resizing if the current size is
close enough).

So, you may find it equally effective to set copy-generational-ratio
to 8.0 (or higher).  This would have the benefit of using a larger
heap, while still forcing minor collections.

The other condition that depends upon runtime options is:
  (((float)(h->size - s->lastMajorStatistics.bytesLive) /
(float)nurserySize) <= s->controls.ratios.nursery
The left-hand-side of the comparison is computing the ratio of the
nurserySize after the last major garbage collection to the current
nurserySize (presumably after a minor collection).  The default
setting of nursery-ratio to 10.0 says to switch back to a major
garbage collection after the nursery has been reduced to one-tenth of
its original size.  So, setting nursery-ratio to a higher value would
stay with minor collections longer.

Given the analysis above, it would seem that with the default values
of live-ratio 8.0 and copy-generational-ratio 4.0, the runtime system
would tend not to switch over to generational garbage collection when
there is plenty of available RAM, because (float)h->size /
(float)s->lastMajorStatistics.bytesLive would be approximately 8.0
(the live-ratio value) and therefore not <= 4.0 (the
copy-generational-ratio).  The runtime system would switch over to
generational garbage collection when the desired heap size does not
fit in RAM and (float)h->size /
(float)s->lastMajorStatistics.bytesLive falls below the live-ratio
value.

I will note that these conditions don't seem to be capturing your
scenario, where there is a large proportion of data that is preserved
across multiple major garbage collections.  I think that the intuition
(at the time the generational gc was implemented and the default
ratios set) was that a program would likely be running short on RAM if
it were performing many garbage collections.  Of course, RAM has
gotten cheaper and I think that it is less often that the runtime
system cannot satisfy the desired heap size.

In any case, the collection of runtime controls and their effects are
fairly complicated.  It would be very nice to review, rationalize, and
refactor the various controls.

> My understanding is that decreasing live-ratio prompts generational
> garbage collection to occur earlier, which greatly cuts gc cost here.
> The attached files show gc-summary for the two values of this option
> on a typical run (in 32 bit).

It certainly forces the generational garbage collection to occur
earlier, but that isn't necessarily always effective:

[mtf@fenrir tmp]$ ./DLXSimulator @MLton gc-summary -- 100 > /dev/null
GC type time ms number  bytes      bytes/sec
------------- ------- ------- --------------- ---------------
copying  3,783  1,837  3,531,238,728    933,449,287
mark-compact      0      0              0              -
minor      0      0              0              -
total time: 16,867 ms
total GC time: 3,826 ms (22.7%)
max pause time: 8 ms
total bytes allocated: 42,872,283,376 bytes
max bytes live: 3,174,432 bytes
max heap size: 25,485,312 bytes
max stack size: 1,408 bytes
num cards marked: 0
bytes scanned: 0 bytes
bytes hash consed: 0 bytes
[mtf@fenrir tmp]$ ./DLXSimulator @MLton gc-summary live-ratio 4.0 --
100 > /dev/null
GC type time ms number  bytes      bytes/sec
------------- ------- ------- --------------- ---------------
copying 12,194  6,780 10,864,430,656    890,965,319
mark-compact      0      0              0              -
minor  3,142  4,082  1,990,329,704    633,459,493
total time: 31,822 ms
total GC time: 16,805 ms (52.8%)
max pause time: 9 ms
total bytes allocated: 42,879,154,136 bytes
max bytes live: 3,174,504 bytes
max heap size: 12,738,560 bytes
max stack size: 1,408 bytes
num cards marked: 4,568
bytes scanned: 6,393,040 bytes
bytes hash consed: 0 bytes
[mtf@fenrir tmp]$ ./DLXSimulator @MLton gc-summary
copy-generational-ratio 8.0 -- 100 > /dev/null
GC type time ms number  bytes      bytes/sec
------------- ------- ------- --------------- ---------------
copying  3,892  1,829  3,519,827,208    904,374,922
mark-compact      0      0              0              -
minor     32     27     20,218,520    631,828,750
total time: 17,219 ms
total GC time: 3,979 ms (23.1%)
max pause time: 8 ms
total bytes allocated: 42,872,270,344 bytes
max bytes live: 3,174,504 bytes
max heap size: 25,485,312 bytes
max stack size: 1,408 bytes
num cards marked: 29
bytes scanned: 42,024 bytes
bytes hash consed: 0 bytes
[mtf@fenrir tmp]$ ./DLXSimulator @MLton gc-summary
copy-generational-ratio 16.0 -- 100 > /dev/null
GC type time ms number  bytes      bytes/sec
------------- ------- ------- --------------- ---------------
copying  1,454    471  1,294,143,408    890,057,331
mark-compact      0      0              0              -
minor 14,896  7,232  9,906,411,512    665,038,367
total time: 29,547 ms
total GC time: 16,460 ms (55.7%)
max pause time: 8 ms
total bytes allocated: 42,870,305,408 bytes
max bytes live: 3,174,936 bytes
max heap size: 25,485,312 bytes
max stack size: 1,408 bytes
num cards marked: 7,702
bytes scanned: 11,444,640 bytes
bytes hash consed: 0 bytes


> 1. Do you agree with this analysis of the improvement ?

Yes.

> 2. Are there some way other than playing with ratios (which could
> have some undesired effects as well) to have mlton always use
> generational gc ? I don't mind having to recompile mlton or the runtime.
>
> 3. Would there be any drawbacks to do so ?

You could recompile the runtime with FORCE_GENERATIONAL set to TRUE
(in runtime/gc/debug.h), but that would effect all programs linked to
that recompiled runtime.  I think that adjusting
copy-generational-ratio is the simplest and safest solution.

------------------------------------------------------------------------------
Want fast and easy access to all the code in your enterprise? Index and
search up to 200,000 lines of code with a free copy of Black Duck
Code Sight - the same software that powers the world's largest code
search on Ohloh, the Black Duck Open Hub! Try it now.
http://p.sf.net/sfu/bds