2 stable releases

1.1.0 Jul 12, 2023
1.0.0 Jul 10, 2023

#295 in Memory management

48 downloads per month

BSD-2-Clause

670KB
16K SLoC

RSGC

Semi-conservative Mark&Sweep Garbage Collector inspired by Shenandoah for Rust written in Rust.

Features

  • Semi-conservative: can find GC pointers on thread stack and registers
  • Supports variable-sized objects (not Rust dyn objects!) like arrays and strings.
  • Light finalizers: cannot revive objects when an object has `DESTRUCTIBLE`` set to true.
  • Finalizers: ordered finalizers that can revive objects.
  • Weak references
  • Weak maps
  • Concurrent
  • Multi-threaded

Benchmarks

To run benchmarks you need .NET and Java installed, enter benchmarks/binarytrees directory and run make.

Architecture

Allocation

Allocation happens in regions, if allocation request size is larger than humongous object threshold then multiple regions are used to allocate object, otherwise object gets allocated in free-list. Fast path also includes per-thread TLAB for speeding up allocation.

Collection Cycle

RSGC essentially has 3 GCs: Concurrent, Degenerated and Full GC. Concurrent GC gets periodically scheduled by background controller thread. If too much allocation happens while concurrent GC runs or OOM happens we switch to degenerated GC. Degenerated GC simply finished Concurrent GC work in stop-the-world phase. If too many degenerated cycles happen we start Full GC. Full GC does GC cycle fully in stop-the-world.

When GC starts?

It depends on heuristics choosen. At the moment only adaptive and compact are available.

  • Adaptive: these heuristics will tune GC threshold based on throughput and latency of collections. Also they account for allocation spikes that can happen at mutator time.
  • Compact: simple heuristics that trigger when certain allocation or free threshold is reached.

Concurrency

RSGC is Concurrent Mark-And-Sweep. This means you need to ensure that objects are properly traced by collector when marking is running. For this Thread::write_barrier should be invoked right after write of field to an object:


fn foo(thread: &mut Thread, obj: Handle<Obj>, field: Handle<Obj2>) {
    obj.as_mut().field = field;
    thread.write_barrier(obj); // possibly push obj to mark worklist
}

Safepoints

Safepoints are the only way GC can synchronize all threads at some point in time to perform some operations that can't be performed concurrently with threads running. For safepoint to happen you should invoke Thread::safepoint:


fn some_expensive_func(thread: &mut Thread) {
    thread.safepoint(); // safepoint at entry

    while /* some condition */ {
        // safepoint at loop header
        thread.safepoint();
        /* some expensive code */
    }
}

Rooting

You do not need to somehow root your objects. GC is able to discover objects on stack automatically. You can use Heap::add_global_root to add walk-able root object in case not all your objects reside on stack.

Dependencies

~4–14MB
~144K SLoC