Skip to content

Commit af30e47

Browse files
committed
[ruby/mmtk] Implement Ruby heap
This heap emulates the growth characteristics of the Ruby default GC's heap. By default, the heap grows by 40%, requires at least 20% empty after a GC, and allows at most 65% empty before it shrinks the heap. This is all configurable via the same environment variables the default GC uses (`RUBY_GC_HEAP_FREE_SLOTS_GOAL_RATIO`, `RUBY_GC_HEAP_FREE_SLOTS_MIN_RATIO`, `RUBY_GC_HEAP_FREE_SLOTS_MAX_RATIO`, respectively). The Ruby heap can be enabled via the `MMTK_HEAP_MODE=ruby` environment variable. Compared to the dynamic heap in MMTk (which uses the MemBalancer algorithm), the Ruby heap allows the heap to grow more generously, which uses a bit more memory but offers significant performance gains because it runs GC much less frequently. We can see in the benchmarks below that this Ruby heap heap gives faster performance than the dynamic heap in every benchmark, with over 2x faster in many of them. We see that memory is often around 10-20% higher with certain outliers that use significantly more memory like hexapdf and erubi-rails. We can also see that this brings MMTk's Ruby heap much closer in performance to the default GC. Ruby heap benchmark results: -------------- -------------- ---------- --------- bench ruby heap (ms) stddev (%) RSS (MiB) activerecord 233.6 10.7 85.9 chunky-png 457.1 1.1 79.3 erubi-rails 1148.0 3.8 133.3 hexapdf 1570.5 2.4 403.0 liquid-c 42.8 5.3 43.4 liquid-compile 41.3 7.6 52.6 liquid-render 102.8 3.8 55.3 lobsters 651.9 8.0 426.3 mail 106.4 1.8 67.2 psych-load 1552.1 0.8 43.4 railsbench 1707.2 6.0 145.6 rubocop 127.2 15.3 148.8 ruby-lsp 136.6 11.7 113.7 sequel 47.2 5.9 44.4 shipit 1197.5 3.6 301.0 -------------- -------------- ---------- --------- Dynamic heap benchmark results: -------------- ----------------- ---------- --------- bench dynamic heap (ms) stddev (%) RSS (MiB) activerecord 845.3 3.1 76.7 chunky-png 525.9 0.4 38.9 erubi-rails 2694.9 3.4 115.8 hexapdf 2344.8 5.6 164.9 liquid-c 73.7 5.0 40.5 liquid-compile 107.1 6.8 40.3 liquid-render 147.2 1.7 39.5 lobsters 697.6 4.5 342.0 mail 224.6 2.1 64.0 psych-load 4326.7 0.6 37.4 railsbench 3218.0 5.5 124.7 rubocop 203.6 6.1 110.9 ruby-lsp 350.7 3.2 79.0 sequel 121.8 2.5 39.6 shipit 1510.1 3.1 220.8 -------------- ----------------- ---------- --------- Default GC benchmark results: -------------- --------------- ---------- --------- bench default GC (ms) stddev (%) RSS (MiB) activerecord 148.4 0.6 67.9 chunky-png 440.2 0.7 57.0 erubi-rails 722.7 0.3 97.8 hexapdf 1466.2 1.7 254.3 liquid-c 32.5 3.6 42.3 liquid-compile 31.2 1.9 35.4 liquid-render 88.3 0.7 30.8 lobsters 633.6 7.0 305.4 mail 76.6 1.6 53.2 psych-load 1166.2 1.3 29.1 railsbench 1262.9 2.3 114.7 rubocop 105.6 0.8 95.4 ruby-lsp 101.6 1.4 75.4 sequel 27.4 1.2 33.1 shipit 1083.1 1.5 163.4 -------------- --------------- ---------- --------- ruby/mmtk@c0ca29922d
1 parent 481f16f commit af30e47

File tree

5 files changed

+175
-4
lines changed

5 files changed

+175
-4
lines changed

gc/mmtk/src/api.rs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use mmtk::util::alloc::BumpPointer;
66
use mmtk::util::alloc::ImmixAllocator;
7+
use mmtk::util::conversions;
78
use mmtk::util::options::PlanSelector;
89
use std::str::FromStr;
910
use std::sync::atomic::Ordering;
@@ -13,6 +14,8 @@ use crate::abi::RubyBindingOptions;
1314
use crate::abi::RubyUpcalls;
1415
use crate::binding;
1516
use crate::binding::RubyBinding;
17+
use crate::heap::RubyHeapTriggerConfig;
18+
use crate::heap::RUBY_HEAP_TRIGGER_CONFIG;
1619
use crate::mmtk;
1720
use crate::utils::default_heap_max;
1821
use crate::utils::parse_capacity;
@@ -79,13 +82,55 @@ fn mmtk_builder_default_parse_heap_max() -> usize {
7982
parse_env_var_with("MMTK_HEAP_MAX", parse_capacity).unwrap_or_else(default_heap_max)
8083
}
8184

85+
fn parse_float_env_var(key: &str, default: f64, min: f64, max: f64) -> f64 {
86+
parse_env_var_with(key, |s| {
87+
let mut float = f64::from_str(s).unwrap_or(default);
88+
89+
if float <= min {
90+
eprintln!(
91+
"{key} has value {float} which must be greater than {min}, using default instead"
92+
);
93+
float = default;
94+
}
95+
96+
if float >= max {
97+
eprintln!(
98+
"{key} has value {float} which must be less than {max}, using default instead"
99+
);
100+
float = default;
101+
}
102+
103+
Some(float)
104+
})
105+
.unwrap_or(default)
106+
}
107+
82108
fn mmtk_builder_default_parse_heap_mode(heap_min: usize, heap_max: usize) -> GCTriggerSelector {
83109
let make_fixed = || GCTriggerSelector::FixedHeapSize(heap_max);
84110
let make_dynamic = || GCTriggerSelector::DynamicHeapSize(heap_min, heap_max);
85111

86112
parse_env_var_with("MMTK_HEAP_MODE", |s| match s {
87113
"fixed" => Some(make_fixed()),
88114
"dynamic" => Some(make_dynamic()),
115+
"ruby" => {
116+
let min_ratio = parse_float_env_var("RUBY_GC_HEAP_FREE_SLOTS_MIN_RATIO", 0.2, 0.0, 1.0);
117+
let goal_ratio =
118+
parse_float_env_var("RUBY_GC_HEAP_FREE_SLOTS_GOAL_RATIO", 0.4, min_ratio, 1.0);
119+
let max_ratio =
120+
parse_float_env_var("RUBY_GC_HEAP_FREE_SLOTS_MAX_RATIO", 0.65, goal_ratio, 1.0);
121+
122+
crate::heap::RUBY_HEAP_TRIGGER_CONFIG
123+
.set(RubyHeapTriggerConfig {
124+
min_heap_pages: conversions::bytes_to_pages_up(heap_min),
125+
max_heap_pages: conversions::bytes_to_pages_up(heap_max),
126+
heap_pages_min_ratio: min_ratio,
127+
heap_pages_goal_ratio: goal_ratio,
128+
heap_pages_max_ratio: max_ratio,
129+
})
130+
.unwrap_or_else(|_| panic!("RUBY_HEAP_TRIGGER_CONFIG is already set"));
131+
132+
Some(GCTriggerSelector::Delegated)
133+
}
89134
_ => None,
90135
})
91136
.unwrap_or_else(make_dynamic)
@@ -146,7 +191,7 @@ pub unsafe extern "C" fn mmtk_init_binding(
146191

147192
crate::set_panic_hook();
148193

149-
let builder = unsafe { Box::from_raw(builder) };
194+
let builder: Box<MMTKBuilder> = unsafe { Box::from_raw(builder) };
150195
let binding_options = RubyBindingOptions {
151196
ractor_check_mode: false,
152197
suffix_size: 0,
@@ -388,11 +433,12 @@ pub extern "C" fn mmtk_plan() -> *const u8 {
388433
pub extern "C" fn mmtk_heap_mode() -> *const u8 {
389434
static FIXED_HEAP: &[u8] = b"fixed\0";
390435
static DYNAMIC_HEAP: &[u8] = b"dynamic\0";
436+
static RUBY_HEAP: &[u8] = b"ruby\0";
391437

392438
match *crate::BINDING.get().unwrap().mmtk.get_options().gc_trigger {
393439
GCTriggerSelector::FixedHeapSize(_) => FIXED_HEAP.as_ptr(),
394440
GCTriggerSelector::DynamicHeapSize(_, _) => DYNAMIC_HEAP.as_ptr(),
395-
_ => panic!("Unknown heap mode"),
441+
GCTriggerSelector::Delegated => RUBY_HEAP.as_ptr(),
396442
}
397443
}
398444

@@ -401,7 +447,12 @@ pub extern "C" fn mmtk_heap_min() -> usize {
401447
match *crate::BINDING.get().unwrap().mmtk.get_options().gc_trigger {
402448
GCTriggerSelector::FixedHeapSize(_) => 0,
403449
GCTriggerSelector::DynamicHeapSize(min_size, _) => min_size,
404-
_ => panic!("Unknown heap mode"),
450+
GCTriggerSelector::Delegated => conversions::pages_to_bytes(
451+
RUBY_HEAP_TRIGGER_CONFIG
452+
.get()
453+
.expect("RUBY_HEAP_TRIGGER_CONFIG not set")
454+
.min_heap_pages,
455+
),
405456
}
406457
}
407458

@@ -410,7 +461,12 @@ pub extern "C" fn mmtk_heap_max() -> usize {
410461
match *crate::BINDING.get().unwrap().mmtk.get_options().gc_trigger {
411462
GCTriggerSelector::FixedHeapSize(max_size) => max_size,
412463
GCTriggerSelector::DynamicHeapSize(_, max_size) => max_size,
413-
_ => panic!("Unknown heap mode"),
464+
GCTriggerSelector::Delegated => conversions::pages_to_bytes(
465+
RUBY_HEAP_TRIGGER_CONFIG
466+
.get()
467+
.expect("RUBY_HEAP_TRIGGER_CONFIG not set")
468+
.max_heap_pages,
469+
),
414470
}
415471
}
416472

gc/mmtk/src/collection.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use crate::abi::GCThreadTLS;
22

33
use crate::api::RubyMutator;
4+
use crate::heap::RubyHeapTrigger;
45
use crate::{mmtk, upcalls, Ruby};
56
use mmtk::memory_manager;
67
use mmtk::scheduler::*;
8+
use mmtk::util::heap::GCTriggerPolicy;
79
use mmtk::util::{VMMutatorThread, VMThread, VMWorkerThread};
810
use mmtk::vm::{Collection, GCThreadContext};
911
use std::sync::atomic::Ordering;
@@ -67,6 +69,10 @@ impl Collection<Ruby> for VMCollection {
6769
fn vm_live_bytes() -> usize {
6870
(upcalls().vm_live_bytes)()
6971
}
72+
73+
fn create_gc_trigger() -> Box<dyn GCTriggerPolicy<Ruby>> {
74+
Box::new(RubyHeapTrigger::default())
75+
}
7076
}
7177

7278
impl VMCollection {

gc/mmtk/src/heap/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
mod ruby_heap_trigger;
2+
pub use ruby_heap_trigger::RubyHeapTrigger;
3+
pub use ruby_heap_trigger::RubyHeapTriggerConfig;
4+
pub use ruby_heap_trigger::RUBY_HEAP_TRIGGER_CONFIG;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use std::sync::atomic::{AtomicUsize, Ordering};
2+
3+
use mmtk::util::heap::GCTriggerPolicy;
4+
use mmtk::util::heap::SpaceStats;
5+
use mmtk::Plan;
6+
use mmtk::MMTK;
7+
use once_cell::sync::OnceCell;
8+
9+
use crate::Ruby;
10+
11+
pub static RUBY_HEAP_TRIGGER_CONFIG: OnceCell<RubyHeapTriggerConfig> = OnceCell::new();
12+
13+
pub struct RubyHeapTriggerConfig {
14+
/// Min heap size
15+
pub min_heap_pages: usize,
16+
/// Max heap size
17+
pub max_heap_pages: usize,
18+
/// Minimum ratio of empty space after a GC before the heap will grow
19+
pub heap_pages_min_ratio: f64,
20+
/// Ratio the heap will grow by
21+
pub heap_pages_goal_ratio: f64,
22+
/// Maximum ratio of empty space after a GC before the heap will shrink
23+
pub heap_pages_max_ratio: f64,
24+
}
25+
26+
pub struct RubyHeapTrigger {
27+
/// Target number of heap pages
28+
target_heap_pages: AtomicUsize,
29+
}
30+
31+
impl GCTriggerPolicy<Ruby> for RubyHeapTrigger {
32+
fn is_gc_required(
33+
&self,
34+
space_full: bool,
35+
space: Option<SpaceStats<Ruby>>,
36+
plan: &dyn Plan<VM = Ruby>,
37+
) -> bool {
38+
// Let the plan decide
39+
plan.collection_required(space_full, space)
40+
}
41+
42+
fn on_gc_end(&self, mmtk: &'static MMTK<Ruby>) {
43+
if let Some(plan) = mmtk.get_plan().generational() {
44+
if plan.is_current_gc_nursery() {
45+
// Nursery GC
46+
} else {
47+
// Full GC
48+
}
49+
50+
panic!("TODO: support for generational GC not implemented")
51+
} else {
52+
let used_pages = mmtk.get_plan().get_used_pages();
53+
54+
let target_min =
55+
(used_pages as f64 * (1.0 + Self::get_config().heap_pages_min_ratio)) as usize;
56+
let target_max =
57+
(used_pages as f64 * (1.0 + Self::get_config().heap_pages_max_ratio)) as usize;
58+
let new_target =
59+
(((used_pages as f64) * (1.0 + Self::get_config().heap_pages_goal_ratio)) as usize)
60+
.clamp(
61+
Self::get_config().min_heap_pages,
62+
Self::get_config().max_heap_pages,
63+
);
64+
65+
if used_pages < target_min || used_pages > target_max {
66+
self.target_heap_pages.store(new_target, Ordering::Relaxed);
67+
}
68+
}
69+
}
70+
71+
fn is_heap_full(&self, plan: &dyn Plan<VM = Ruby>) -> bool {
72+
plan.get_reserved_pages() > self.target_heap_pages.load(Ordering::Relaxed)
73+
}
74+
75+
fn get_current_heap_size_in_pages(&self) -> usize {
76+
self.target_heap_pages.load(Ordering::Relaxed)
77+
}
78+
79+
fn get_max_heap_size_in_pages(&self) -> usize {
80+
Self::get_config().max_heap_pages
81+
}
82+
83+
fn can_heap_size_grow(&self) -> bool {
84+
self.target_heap_pages.load(Ordering::Relaxed) < Self::get_config().max_heap_pages
85+
}
86+
}
87+
88+
impl Default for RubyHeapTrigger {
89+
fn default() -> Self {
90+
let min_heap_pages = Self::get_config().min_heap_pages;
91+
92+
Self {
93+
target_heap_pages: AtomicUsize::new(min_heap_pages),
94+
}
95+
}
96+
}
97+
98+
impl RubyHeapTrigger {
99+
fn get_config<'b>() -> &'b RubyHeapTriggerConfig {
100+
RUBY_HEAP_TRIGGER_CONFIG
101+
.get()
102+
.expect("Attempt to use RUBY_HEAP_TRIGGER_CONFIG before it is initialized")
103+
}
104+
}

gc/mmtk/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod active_plan;
2525
pub mod api;
2626
pub mod binding;
2727
pub mod collection;
28+
pub mod heap;
2829
pub mod object_model;
2930
pub mod reference_glue;
3031
pub mod scanning;

0 commit comments

Comments
 (0)