Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ This repository holds the [MMTk](https://www.mmtk.io/) bindings for Ruby. The bi
After building Ruby and the MMTk bindings, run Ruby with `RUBY_GC_LIBRARY=mmtk` environment variable. You can also configure the following environment variables:

- `MMTK_PLAN=<NoGC|MarkSweep|Immix>`: Configures the GC algorithm used by MMTk. Defaults to `Immix`.
- `MMTK_HEAP_MODE=<fixed|dynamic>`: Configures the MMTk heap used. `fixed` is a fixed size heap, `dynamic` is a dynamic sized heap that will grow and shrink in size based on heuristics using the [MemBalancer](https://dl.acm.org/doi/pdf/10.1145/3563323) algorithm. Defaults to `dynamic`.
- `MMTK_HEAP_MIN=<size>`: Configures the lower bound in heap memory usage by MMTk. Only valid when `MMTK_HEAP_MODE=dynamic`. `size` is in bytes, but you can also append `KiB`, `MiB`, `GiB` for larger sizes. Defaults to 1MiB.
- `MMTK_HEAP_MODE=<fixed|dynamic|ruby|cpu>`: Configures the MMTk heap used. Defaults to `dynamic`.
- `fixed`: a fixed size heap.
- `dynamic`: a dynamic sized heap that will grow and shrink in size based on heuristics using the [MemBalancer](https://dl.acm.org/doi/pdf/10.1145/3563323) algorithm.
- `ruby`: a dynamic sized heap that grows and shrinks based on the ratio of free to used slots, using the same `RUBY_GC_HEAP_FREE_SLOTS_*_RATIO` env vars as the default Ruby GC.
- `cpu`: a dynamic sized heap that adjusts itself to hit a target GC CPU overhead, using the algorithm from [Tavakolisomeh et al., "Heap Size Adjustment with CPU Control" (MPLR '23)](https://dl.acm.org/doi/10.1145/3617651.3622988). Tunable via `MMTK_GC_CPU_TARGET` and `MMTK_GC_CPU_WINDOW` (see below).
- `MMTK_HEAP_MIN=<size>`: Configures the lower bound in heap memory usage by MMTk. Only valid when `MMTK_HEAP_MODE` is `dynamic`, `ruby`, or `cpu`. `size` is in bytes, but you can also append `KiB`, `MiB`, `GiB` for larger sizes. Defaults to 1MiB.
- `MMTK_HEAP_MAX=<size>`: Configures the upper bound in heap memory usage by MMTk. Once this limit is reached and no objects can be garbage collected, it will crash with an out-of-memory. `size` is in bytes, but you can also append `KiB`, `MiB`, `GiB` for larger sizes. Defaults to 80% of your system RAM.
- `MMTK_GC_CPU_TARGET=<percent>`: Target GC CPU overhead, as a percentage, when `MMTK_HEAP_MODE=cpu`. After each GC cycle, the heap is grown if the measured GC CPU overhead exceeds this target and shrunk if it falls below. Defaults to `5`. The paper recommends `15` for the concurrent collector it targets (ZGC), but on MMTk-Ruby's stop-the-world Immix every percent of GC CPU also blocks the mutator, so a smaller budget gives better throughput. Empirical sweeps across ruby-bench find 5 Pareto-optimal vs. the `ruby` heap mode (~6% geomean speedup at essentially equal peak RSS).
- `MMTK_GC_CPU_WINDOW=<n>`: Number of recent GC cycles averaged when measuring GC CPU overhead for `MMTK_HEAP_MODE=cpu`. Larger values smooth the signal at the cost of responsiveness. Defaults to `3`.
85 changes: 72 additions & 13 deletions gc/mmtk/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ use crate::abi::RubyBindingOptions;
use crate::abi::RubyUpcalls;
use crate::binding;
use crate::binding::RubyBinding;
use crate::heap::CpuHeapTriggerConfig;
use crate::heap::RubyHeapTriggerConfig;
use crate::heap::CPU_HEAP_TRIGGER_CONFIG;
use crate::heap::RUBY_HEAP_TRIGGER_CONFIG;
use crate::mmtk;
use crate::utils::default_heap_max;
Expand Down Expand Up @@ -131,6 +133,42 @@ fn mmtk_builder_default_parse_heap_mode(heap_min: usize, heap_max: usize) -> GCT

Some(GCTriggerSelector::Delegated)
}
"cpu" => {
// CPU-overhead-driven heap sizing based on Tavakolisomeh et al.,
// "Heap Size Adjustment with CPU Control", MPLR '23.
//
// Target is expressed as a percentage (0, 100) via
// `MMTK_GC_CPU_TARGET`. The paper recommends 15 for ZGC (a
// concurrent collector); we default to 5 for MMTk-Ruby. With
// MMTk's stop-the-world Immix, every percent of GC CPU is also
// a percent of wall-clock the mutator is blocked on, so a much
// smaller budget is appropriate. An empirical sweep across
// ruby-bench (railsbench, lobsters, psych-load, liquid-render,
// lee) found target=5 to be Pareto-optimal: ~6% geomean speedup
// vs. the `ruby` heap mode with effectively identical geomean
// peak RSS.
let target_percent = parse_float_env_var("MMTK_GC_CPU_TARGET", 5.0, 0.0, 100.0);
let window_size = parse_env_var::<usize>("MMTK_GC_CPU_WINDOW").unwrap_or(3);
let window_size = window_size.max(1);

let min_heap_pages = conversions::bytes_to_pages_up(heap_min);
let max_heap_pages = conversions::bytes_to_pages_up(heap_max);
// Start at the min heap size, as the other delegated triggers do.
// The control loop will adjust from here after the first GC cycle.
let initial_heap_pages = min_heap_pages;

CPU_HEAP_TRIGGER_CONFIG
.set(CpuHeapTriggerConfig {
min_heap_pages,
max_heap_pages,
initial_heap_pages,
target_gc_cpu: target_percent / 100.0,
window_size,
})
.unwrap_or_else(|_| panic!("CPU_HEAP_TRIGGER_CONFIG is already set"));

Some(GCTriggerSelector::Delegated)
}
_ => None,
})
.unwrap_or_else(make_dynamic)
Expand Down Expand Up @@ -446,11 +484,20 @@ pub extern "C" fn mmtk_heap_mode() -> *const u8 {
static FIXED_HEAP: &[u8] = b"fixed\0";
static DYNAMIC_HEAP: &[u8] = b"dynamic\0";
static RUBY_HEAP: &[u8] = b"ruby\0";
static CPU_HEAP: &[u8] = b"cpu\0";

match *crate::BINDING.get().unwrap().mmtk.get_options().gc_trigger {
GCTriggerSelector::FixedHeapSize(_) => FIXED_HEAP.as_ptr(),
GCTriggerSelector::DynamicHeapSize(_, _) => DYNAMIC_HEAP.as_ptr(),
GCTriggerSelector::Delegated => RUBY_HEAP.as_ptr(),
GCTriggerSelector::Delegated => {
// Two delegated triggers exist; disambiguate via the populated
// config singleton.
if CPU_HEAP_TRIGGER_CONFIG.get().is_some() {
CPU_HEAP.as_ptr()
} else {
RUBY_HEAP.as_ptr()
}
}
}
}

Expand All @@ -459,12 +506,18 @@ pub extern "C" fn mmtk_heap_min() -> usize {
match *crate::BINDING.get().unwrap().mmtk.get_options().gc_trigger {
GCTriggerSelector::FixedHeapSize(_) => 0,
GCTriggerSelector::DynamicHeapSize(min_size, _) => min_size,
GCTriggerSelector::Delegated => conversions::pages_to_bytes(
RUBY_HEAP_TRIGGER_CONFIG
.get()
.expect("RUBY_HEAP_TRIGGER_CONFIG not set")
.min_heap_pages,
),
GCTriggerSelector::Delegated => {
if let Some(cfg) = CPU_HEAP_TRIGGER_CONFIG.get() {
conversions::pages_to_bytes(cfg.min_heap_pages)
} else {
conversions::pages_to_bytes(
RUBY_HEAP_TRIGGER_CONFIG
.get()
.expect("RUBY_HEAP_TRIGGER_CONFIG not set")
.min_heap_pages,
)
}
}
}
}

Expand All @@ -473,12 +526,18 @@ pub extern "C" fn mmtk_heap_max() -> usize {
match *crate::BINDING.get().unwrap().mmtk.get_options().gc_trigger {
GCTriggerSelector::FixedHeapSize(max_size) => max_size,
GCTriggerSelector::DynamicHeapSize(_, max_size) => max_size,
GCTriggerSelector::Delegated => conversions::pages_to_bytes(
RUBY_HEAP_TRIGGER_CONFIG
.get()
.expect("RUBY_HEAP_TRIGGER_CONFIG not set")
.max_heap_pages,
),
GCTriggerSelector::Delegated => {
if let Some(cfg) = CPU_HEAP_TRIGGER_CONFIG.get() {
conversions::pages_to_bytes(cfg.max_heap_pages)
} else {
conversions::pages_to_bytes(
RUBY_HEAP_TRIGGER_CONFIG
.get()
.expect("RUBY_HEAP_TRIGGER_CONFIG not set")
.max_heap_pages,
)
}
}
}
}

Expand Down
13 changes: 12 additions & 1 deletion gc/mmtk/src/collection.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::abi::GCThreadTLS;

use crate::api::RubyMutator;
use crate::heap::CpuHeapTrigger;
use crate::heap::RubyHeapTrigger;
use crate::heap::CPU_HEAP_TRIGGER_CONFIG;
use crate::mmtk;
use crate::upcalls;
use crate::Ruby;
Expand Down Expand Up @@ -95,7 +97,16 @@ impl Collection<Ruby> for VMCollection {
}

fn create_gc_trigger() -> Box<dyn GCTriggerPolicy<Ruby>> {
Box::new(RubyHeapTrigger::default())
// `GCTriggerSelector::Delegated` is currently used by two different
// heap modes: `ruby` (the Ruby-like free-slot ratio trigger) and `cpu`
// (the CPU-overhead trigger from Tavakolisomeh et al., MPLR '23).
// Which one is active is determined by which `OnceCell` config the
// `MMTK_HEAP_MODE` parser populated.
if CPU_HEAP_TRIGGER_CONFIG.get().is_some() {
Box::new(CpuHeapTrigger::default())
} else {
Box::new(RubyHeapTrigger::default())
}
}
}

Expand Down
Loading
Loading