State of compaction in OCaml 5?

My understanding is that compaction in the earliest release of OCaml 5 either doesn’t exist or at least doesn’t ever return unused memory to the operating system. However, I couldn’t find a very explicit reference for this; these release notes for OCaml 5.1 mention that “GC compaction is a work in progress,” and there is also an unmerged PR documenting that compaction is not implemented.

On the other hand the 5.2 alpha includes this PR which “reintroduces compaction for the pools in the GC’s major heap” but with a couple of caveats that I don’t know nearly enough about the GC understand: “small blocks < 128 words” and “Explicit only on calls to Gc.compact for now.”

Questions:

  1. Is my understanding of the pre-5.2 situation correct?
  2. Starting with 5.2, does unused memory get returned to the OS?
  3. How significant are the caveats I quoted?

Personally my needs around the GC behavior are (I hope) not too sophisticated. I have a long-running process that parses some large-ish JSON objects at startup and then extracts the useful bits into a much more space-efficient representation for the rest of its lifetime. Since I’m trying to be a bit stingy with memory usage, I’m wondering if 5.2 would be the right time to upgrade to OCaml 5.

2 Likes

Your understanding of pre-5.2 is correct and 5.2’s compaction returns unused memory to the OS (ocaml/runtime/shared_heap.c at trunk · ocaml/ocaml · GitHub )

For context, in OCaml 5’s GC small blocks (those less than 128 words) are managed using size-segregated pools e.g there’s a pool, per domain, that handles allocations of size 3 words, another one that handles size 4 words, etc… This means allocation is fast, we don’t have to look for gap in memory to fit an allocation, we simply look up the right size of pool and follow the convenient pointer to the next free space in it.

For allocations 128 words or larger, we rely on malloc. This can be more expensive but in general, large allocations are done far less frequently. When blocks of 128 words are no longer reachable and they are swept by the GC, they are free’d and so hopefully they eventually get returned to the OS. This happens pre-5.2 as well.

Compaction in 5.2 compacts the pools for smaller allocations and tries to reduce them to only as many pools as necessary to fit blocks of that size. The pools no longer required are released back to the OS.

The 4.x runtime can do compactions automatically if it determines there’s excessive free space, we haven’t enabled that in 5.x at the moment because it’s not entirely clear we’d want to use the new compaction algorithm automatically. Also it’s new and tricky code.

If you are going to upgrade to 5.x, it may be worth playing with the GC space overhead which lets you trade-off memory for CPU.

4 Likes

I don’t think this is in any formal standard, but in most Unix like systems free() doesn’t return the chunk which it acts on to the “system” – as I understand the term – it only makes it available for future invocations of malloc() and friends. But maybe this is what you meant?


Ian

It really depends on the libc. For example, the GNU C Library invokes munmap as soon as the freed memory exceeds a certain threshold. Consider the following simple C code:

#include <stdlib.h>
int main() {
  char *p = malloc(100000000);
  char *q = malloc(100000000);
  free(p);
  free(q);
  return 0;
}

and the corresponding system trace:

brk(0x5605fe4a7000)                     = 0x5605fe4a7000
mmap(NULL, 100003840, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7eff7cca1000
mmap(NULL, 100003840, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7eff76d42000
munmap(0x7eff7cca1000, 100003840)       = 0
munmap(0x7eff76d42000, 100003840)       = 0
exit_group(0)                           = ?

As you can see, every call to free causes a call to munmap.

3 Likes