Fil-C

Memory SafetyC/C++ CompatibilityModern Tooling

Fil's Unbelievable Garbage Collector

Fil-C uses a parallel concurrent on-the-fly grey-stack Dijkstra accurate non-moving garbage collector called FUGC (Fil's Unbelievable Garbage Collector). You can find the source code for the collector itself in fugc.c, though be warned, that code cannot possibly work without lots of support logic in the rest of the runtime and in the compiler.

Let's break down FUGC's features:

This makes FUGC an advancing wavefront garbage collector. Advancing wavefront means that the mutator cannot create new work for the collector by modifying the heap. Once an object is marked, it'll stay marked for that GC cycle. It's also an incremental update collector, since some objects that would have been live at the start of GC might get freed if they become free during the collection cycle.

FUGC relies on safepoints, which comprise:

Safepointing is essential for supporting threading (Fil-C supports pthreads just fine) while avoiding a large class of race conditions. For example, safepointing means that it's safe to load a pointer from the heap and then use it; the GC cannot possibly delete that memory until the next pollcheck or exit. So, the compiler and runtime just have to ensure that the pointer becomes tracked for stack scanning at some point between when it's loaded and when the next pollcheck/exit happens, and only if the pointer is still live at that point.

The safepointing functionality also supports stop-the-world, which is currently used to implement fork(2) and for debugging FUGC (if you set the FUGC_STW environment variable to 1 then the collector will stop the world and this is useful for triaging GC bugs; if the bug reproduces in STW then it means it's not due to issues with the store barrier). The safepoint infrastructure also allows safe signal delivery; Fil-C makes it possible to use signal handling in a practical way. Safepointing is a common feature of virtual machines that support multiple threads and accurate garbage collection, though usually, they are only used to stop the world rather than to request asynchronous activity from all threads. See here for a write-up about how OpenJDK does it. The Fil-C implementation is in filc_runtime.c.

Here's the basic flow of the FUGC collector loop:

  1. Wait for the GC trigger.
  2. Turn on the store barrier, then soft handshake with a no-op callback.
  3. Turn on black allocation (new objects get allocated marked), then soft handshake with a callback that resets thread-local caches.
  4. Mark global roots.
  5. Soft handshake with a callback that requests stack scan and another reset of thread-local caches. If all collector mark stacks are empty after this, go to step 7.
  6. Tracing: for each object in the mark stack, mark its outgoing references (which may grow the mark stack). Do this until the mark stack is empty. Then go to step 5.
  7. Turn off the store barrier and prepare for sweeping, then soft handshake to reset thread-local caches again.
  8. Perform the sweep. During the sweep, objects are allocated black if they happen to be allocated out of not-yet-swept pages, or white if they are allocated out of alraedy-swept pages.
  9. Victory! Go back to step 1.

If you're familiar with the literature, FUGC is sort of like the DLG (Doligez-Leroy-Gonthier) collector (published in two papers because they had a serious bug in the first one), except it uses the Dijkstra barrier and a grey stack, which simplifies everything but isn't as academically pure (FUGC fixpoints, theirs doesn't). I first came up with the grey-stack Dijkstra approach when working on Fiji VM's CMR and Schism garbage collectors. The main advantage of FUGC over DLG is that it has a simpler (cheaper) store barrier and it's a slightly more intuitive algorithm. While the fixpoint seems like a disadvantage, in practice it converges after a few iterations.

Additionally, FUGC relies on a sweeping algorithm based on bitvector SIMD. This makes sweeping insanely fast compared to marking. This is made thanks to the Verse heap config that I added to libpas. FUGC typically spends <5% of its time sweeping.

Bonus Features

FUGC supports a most of C-style, Java-style, and JavaScript-style memory management. Let's break down what that means.

Freeing Objects

If you call free, the runtime will flag the object as free and all subsequent accesses to the object will trap. Additionally, FUGC will not scan outgoing references from the object (since they cannot be accessed anymore).

Also, FUGC will redirect all capability pointers (lowers in InvisiCaps jargon) to free objects to point at the free singleton object instead. This allows freed object memory to really be reclaimed.

This means that freeing objects can be used to prevent GC-induced leaks. Surprisingly, a program that works fine with malloc/free (no leaks, no crashes) that gets converted to GC the naive way (malloc allocates from the GC and free is a no-op) may end up leaking due to dangling pointers that the program never accesses. Those dangling pointers will be treated as live by the GC. In FUGC, if you freed those pointers, then FUGC will really kill them.

Finalizers

FUGC supports finalizer queues using the zgc_finq API in stdfil.h. This feature allows you to implement finalizers in the style of Java, except that you get to set up your own finalizer queues and choose which thread processes them.

Weak References

FUGC supports weak references using the zweak API in stdfil.h. Weak references work just like the weak references in Java, except there are no reference queues. Fil-C does not support phantom or soft references.

Weak Maps

FUGC supports weak maps using the zweak_map API in stdfil.h. This API works almost exactly like the JavaScript WeakMap, except that Fil-C's weak maps allow you to iterate all of their elements and get a count of elements.

Conclusion

FUGC allows Fil-C to give the strongest possible guarantees on misuse of free: