From 79cfcb6067677d2398d5304cd8579e8928f692c4 Mon Sep 17 00:00:00 2001 From: Yi Lin Date: Wed, 17 Apr 2024 12:24:32 +1200 Subject: [PATCH] Allow nursery size to be proportional to the heap size (#1087) This PR introduces different kinds of nursery size options, and by default, we use a proportion of the heap size as the min nursery. This PR should generally improve the generational plans' performance by triggering a full heap GC more promptly. This PR mitigates the issue identified in https://github.com/mmtk/mmtk-core/issues/594, but does not fully fix the problem. --- src/plan/generational/global.rs | 10 +- src/plan/generational/mod.rs | 4 +- src/plan/sticky/immix/global.rs | 4 +- src/util/heap/gc_trigger.rs | 67 +++++++- src/util/options.rs | 266 +++++++++++++++++++++----------- 5 files changed, 244 insertions(+), 107 deletions(-) diff --git a/src/plan/generational/global.rs b/src/plan/generational/global.rs index 6f92831dab..86c39a07e8 100644 --- a/src/plan/generational/global.rs +++ b/src/plan/generational/global.rs @@ -40,11 +40,7 @@ pub struct CommonGenPlan { impl CommonGenPlan { pub fn new(mut args: CreateSpecificPlanArgs) -> Self { let nursery = CopySpace::new( - args.get_space_args( - "nursery", - true, - VMRequest::fixed_extent(args.global_args.options.get_max_nursery_bytes(), false), - ), + args.get_space_args("nursery", true, VMRequest::discontiguous()), true, ); let full_heap_gc_count = args @@ -102,7 +98,7 @@ impl CommonGenPlan { space: Option>, ) -> bool { let cur_nursery = self.nursery.reserved_pages(); - let max_nursery = self.common.base.options.get_max_nursery_pages(); + let max_nursery = self.common.base.gc_trigger.get_max_nursery_pages(); let nursery_full = cur_nursery >= max_nursery; trace!( "nursery_full = {:?} (nursery = {}, max_nursery = {})", @@ -261,7 +257,7 @@ impl CommonGenPlan { /// whose value depends on which spaces have been released. pub fn should_next_gc_be_full_heap(plan: &dyn Plan) -> bool { let available = plan.get_available_pages(); - let min_nursery = plan.base().options.get_min_nursery_pages(); + let min_nursery = plan.base().gc_trigger.get_min_nursery_pages(); let next_gc_full_heap = available < min_nursery; trace!( "next gc will be full heap? {}, available pages = {}, min nursery = {}", diff --git a/src/plan/generational/mod.rs b/src/plan/generational/mod.rs index 95aaa48928..2bb61dc55f 100644 --- a/src/plan/generational/mod.rs +++ b/src/plan/generational/mod.rs @@ -48,10 +48,8 @@ pub const GEN_CONSTRAINTS: PlanConstraints = PlanConstraints { barrier: ACTIVE_BARRIER, // We may trace duplicate edges in sticky immix (or any plan that uses object remembering barrier). See https://github.com/mmtk/mmtk-core/issues/743. may_trace_duplicate_edges: ACTIVE_BARRIER.equals(BarrierSelector::ObjectBarrier), - max_non_los_default_alloc_bytes: crate::util::rust_util::min_of_usize( + max_non_los_default_alloc_bytes: crate::plan::plan_constraints::MAX_NON_LOS_ALLOC_BYTES_COPYING_PLAN, - crate::util::options::NURSERY_SIZE, - ), needs_prepare_mutator: false, ..PlanConstraints::default() }; diff --git a/src/plan/sticky/immix/global.rs b/src/plan/sticky/immix/global.rs index f913e2e4b8..f2d2077502 100644 --- a/src/plan/sticky/immix/global.rs +++ b/src/plan/sticky/immix/global.rs @@ -140,8 +140,8 @@ impl Plan for StickyImmix { } fn collection_required(&self, space_full: bool, space: Option>) -> bool { - let nursery_full = - self.immix.immix_space.get_pages_allocated() > self.options().get_max_nursery_pages(); + let nursery_full = self.immix.immix_space.get_pages_allocated() + > self.base().gc_trigger.get_max_nursery_pages(); if space_full && space.is_some() && space.as_ref().unwrap().0.name() != self.immix.immix_space.name() diff --git a/src/util/heap/gc_trigger.rs b/src/util/heap/gc_trigger.rs index e10a7a2db6..94ba63fe6e 100644 --- a/src/util/heap/gc_trigger.rs +++ b/src/util/heap/gc_trigger.rs @@ -4,8 +4,9 @@ use crate::global_state::GlobalState; use crate::plan::gc_requester::GCRequester; use crate::plan::Plan; use crate::policy::space::Space; +use crate::util::constants::BYTES_IN_PAGE; use crate::util::conversions; -use crate::util::options::{GCTriggerSelector, Options}; +use crate::util::options::{GCTriggerSelector, Options, DEFAULT_MAX_NURSERY, DEFAULT_MIN_NURSERY}; use crate::vm::VMBinding; use crate::MMTK; use std::mem::MaybeUninit; @@ -58,6 +59,10 @@ impl GCTrigger { self.plan.write(plan); } + fn plan(&self) -> &dyn Plan { + unsafe { self.plan.assume_init() } + } + /// This method is called periodically by the allocation subsystem /// (by default, each time a page is consumed), and provides the /// collector with an opportunity to collect. @@ -101,8 +106,62 @@ impl GCTrigger { /// Check if the heap is full pub fn is_heap_full(&self) -> bool { - let plan = unsafe { self.plan.assume_init() }; - self.policy.is_heap_full(plan) + self.policy.is_heap_full(self.plan()) + } + + /// Return upper bound of the nursery size (in number of bytes) + pub fn get_max_nursery_bytes(&self) -> usize { + use crate::util::options::NurserySize; + debug_assert!(self.plan().generational().is_some()); + match *self.options.nursery { + NurserySize::Bounded { min: _, max } => max, + NurserySize::ProportionalBounded { min: _, max } => { + let heap_size_bytes = + conversions::pages_to_bytes(self.policy.get_current_heap_size_in_pages()); + let max_bytes = heap_size_bytes as f64 * max; + let max_bytes = conversions::raw_align_up(max_bytes as usize, BYTES_IN_PAGE); + if max_bytes > DEFAULT_MAX_NURSERY { + warn!("Proportional nursery with max size {} ({}) is larger than DEFAULT_MAX_NURSERY ({}). Use DEFAULT_MAX_NURSERY instead.", max, max_bytes, DEFAULT_MAX_NURSERY); + DEFAULT_MAX_NURSERY + } else { + max_bytes + } + } + NurserySize::Fixed(sz) => sz, + } + } + + /// Return lower bound of the nursery size (in number of bytes) + pub fn get_min_nursery_bytes(&self) -> usize { + use crate::util::options::NurserySize; + debug_assert!(self.plan().generational().is_some()); + match *self.options.nursery { + NurserySize::Bounded { min, max: _ } => min, + NurserySize::ProportionalBounded { min, max: _ } => { + let min_bytes = + conversions::pages_to_bytes(self.policy.get_current_heap_size_in_pages()) + as f64 + * min; + let min_bytes = conversions::raw_align_up(min_bytes as usize, BYTES_IN_PAGE); + if min_bytes < DEFAULT_MIN_NURSERY { + warn!("Proportional nursery with min size {} ({}) is smaller than DEFAULT_MIN_NURSERY ({}). Use DEFAULT_MIN_NURSERY instead.", min, min_bytes, DEFAULT_MIN_NURSERY); + DEFAULT_MIN_NURSERY + } else { + min_bytes + } + } + NurserySize::Fixed(sz) => sz, + } + } + + /// Return upper bound of the nursery size (in number of pages) + pub fn get_max_nursery_pages(&self) -> usize { + crate::util::conversions::bytes_to_pages_up(self.get_max_nursery_bytes()) + } + + /// Return lower bound of the nursery size (in number of pages) + pub fn get_min_nursery_pages(&self) -> usize { + crate::util::conversions::bytes_to_pages_up(self.get_min_nursery_bytes()) } } @@ -433,7 +492,7 @@ impl GCTriggerPolicy for MemBalancerTrigger { // We reserve an extra of min nursery. This ensures that we will not trigger // a full heap GC in the next GC (if available pages is smaller than min nursery, we will force a full heap GC) mmtk.get_plan().get_collection_reserved_pages() - + mmtk.options.get_min_nursery_pages(), + + mmtk.gc_trigger.get_min_nursery_pages(), stats, ); } diff --git a/src/util/options.rs b/src/util/options.rs index fb51d0fc65..3d1e70a965 100644 --- a/src/util/options.rs +++ b/src/util/options.rs @@ -6,8 +6,6 @@ use std::fmt::Debug; use std::str::FromStr; use strum_macros::EnumString; -use super::heap::vm_layout::vm_layout; - /// The default stress factor. This is set to the max usize, /// which means we will never trigger a stress GC for the default value. pub const DEFAULT_STRESS_FACTOR: usize = usize::max_value(); @@ -97,9 +95,6 @@ impl FromStr for PerfEventOptions { } } -/// The default nursery space size. -#[cfg(target_pointer_width = "64")] -pub const NURSERY_SIZE: usize = (1 << 20) << LOG_BYTES_IN_MBYTE; /// The default min nursery size. This does not affect the actual space we create as nursery. It is /// only used in the GC trigger check. #[cfg(target_pointer_width = "64")] @@ -109,19 +104,22 @@ pub const DEFAULT_MIN_NURSERY: usize = 2 << LOG_BYTES_IN_MBYTE; #[cfg(target_pointer_width = "64")] pub const DEFAULT_MAX_NURSERY: usize = (1 << 20) << LOG_BYTES_IN_MBYTE; -/// The default nursery space size. -#[cfg(target_pointer_width = "32")] -pub const NURSERY_SIZE: usize = 32 << LOG_BYTES_IN_MBYTE; /// The default min nursery size. This does not affect the actual space we create as nursery. It is /// only used in the GC trigger check. #[cfg(target_pointer_width = "32")] pub const DEFAULT_MIN_NURSERY: usize = 2 << LOG_BYTES_IN_MBYTE; -const DEFAULT_MAX_NURSERY_32: usize = 32 << LOG_BYTES_IN_MBYTE; +/// The default max nursery size for 32 bits. +pub const DEFAULT_MAX_NURSERY_32: usize = 32 << LOG_BYTES_IN_MBYTE; /// The default max nursery size. This does not affect the actual space we create as nursery. It is /// only used in the GC trigger check. #[cfg(target_pointer_width = "32")] pub const DEFAULT_MAX_NURSERY: usize = DEFAULT_MAX_NURSERY_32; +/// The default min nursery size proportional to the current heap size +pub const DEFAULT_PROPORTIONAL_MIN_NURSERY: f64 = 0.25; +/// The default max nursery size proportional to the current heap size +pub const DEFAULT_PROPORTIONAL_MAX_NURSERY: f64 = 1.0; + fn always_valid(_: &T) -> bool { true } @@ -295,6 +293,15 @@ macro_rules! options { ] } +impl Options { + /// Check if the options are set for stress GC. If either stress_factor or analysis_factor is set, + /// we should do stress GC. + pub fn is_stress_test_gc_enabled(&self) -> bool { + *self.stress_factor != DEFAULT_STRESS_FACTOR + || *self.analysis_factor != DEFAULT_STRESS_FACTOR + } +} + #[derive(Clone, Debug, PartialEq)] /// AffinityKind describes how to set the affinity of GC threads. Note that we currently assume /// that each GC thread is equivalent to an OS or hardware thread. @@ -389,105 +396,181 @@ impl FromStr for AffinityKind { } } -#[derive(Copy, Clone, EnumString, Debug)] -/// Different nursery types. -pub enum NurseryKind { +#[derive(Copy, Clone, Debug)] +/// An option that provides a min/max interface to MMTk and a Bounded/Fixed interface to the +/// user/VM. +pub enum NurserySize { /// A Bounded nursery has different upper and lower bounds. The size only controls the upper - /// bound. Hence, it is considered to be a "variable size" nursery. By default, a Bounded - /// nursery has a lower bound of 2 MB and an upper bound of 32 MB for 32-bit systems and 1 TB - /// for 64-bit systems. - Bounded, + /// bound. Hence, it is considered to be a "variable size" nursery. + Bounded { + /// The lower bound of the nursery size in bytes. Default to [`DEFAULT_MIN_NURSERY`]. + min: usize, + /// The upper bound of the nursery size in bytes. Default to [`DEFAULT_MAX_NURSERY`]. + max: usize, + }, + /// A bounded nursery that is porportional to the current heap size. + ProportionalBounded { + /// The lower bound of the nursery size as a proportion of the current heap size. Default to [`DEFAULT_PROPORTIONAL_MIN_NURSERY`]. + min: f64, + /// The upper bound of the nursery size as a proportion of the current heap size. Default to [`DEFAULT_PROPORTIONAL_MAX_NURSERY`]. + max: f64, + }, /// A Fixed nursery has the same upper and lower bounds. The size controls both the upper and /// lower bounds. Note that this is considered less performant than a Bounded nursery since a /// Fixed nursery size can be too restrictive and cause more GCs. - Fixed, -} - -#[derive(Copy, Clone, Debug)] -/// An option that provides a min/max interface to MMTk and a Bounded/Fixed interface to the -/// user/VM. -pub struct NurserySize { - /// The nursery type - pub kind: NurseryKind, - /// Minimum nursery size (in bytes) - pub min: usize, - /// Maximum nursery size (in bytes) - max: Option, + Fixed(usize), } impl NurserySize { - /// Create a NurserySize with the given kind. The value argument specifies the nursery size - /// for a fixed nursery, or specifies the max size for a bounded nursery. - pub fn new(kind: NurseryKind, value: Option) -> Self { - match kind { - NurseryKind::Bounded => NurserySize { - kind, - min: DEFAULT_MIN_NURSERY, - max: value, - }, - NurseryKind::Fixed => NurserySize { - kind, - min: value.unwrap(), - max: value, - }, + /// Return true if the values are valid. + fn validate(&self) -> bool { + match *self { + NurserySize::Bounded { min, max } => min <= max, + NurserySize::ProportionalBounded { min, max } => { + 0.0f64 < min && min <= max && max <= 1.0f64 + } + NurserySize::Fixed(_) => true, } } - - /// Returns a [`NurserySize`] or [`String`] containing error. Expects nursery size to be formatted as - /// `:`. For example, `Fixed:8192` creates a [`NurseryKind::Fixed`] nursery of size - /// 8192 bytes. - pub fn parse(s: &str) -> Result { - let ns: Vec<&str> = s.split(':').collect(); - let kind = ns[0].parse::().map_err(|_| { - String::from("Please specify one of \"Bounded\" or \"Fixed\" nursery type") - })?; - let value = ns[1] - .parse() - .map_err(|_| String::from("Failed to parse size"))?; - Ok(NurserySize::new(kind, Some(value))) - } } impl FromStr for NurserySize { type Err = String; fn from_str(s: &str) -> Result { - NurserySize::parse(s) - } -} + let parts: Vec<&str> = s.split(':').collect(); + if parts.len() != 2 { + return Err("Invalid format".to_string()); + } -impl Options { - /// Return upper bound of the nursery size (in number of bytes) - pub fn get_max_nursery_bytes(&self) -> usize { - self.nursery.max.unwrap_or_else(|| { - if !vm_layout().force_use_contiguous_spaces { - DEFAULT_MAX_NURSERY_32 + let variant = parts[0]; + let values: Vec<&str> = parts[1].split(',').collect(); + + fn default_or_parse(val: &str, default_value: T) -> Result { + if val == "_" { + Ok(default_value) } else { - DEFAULT_MAX_NURSERY + val.parse::() + .map_err(|_| format!("Failed to parse {:?}", std::any::type_name::())) } - }) - } + } - /// Return upper bound of the nursery size (in number of pages) - pub fn get_max_nursery_pages(&self) -> usize { - crate::util::conversions::bytes_to_pages_up(self.get_max_nursery_bytes()) + match variant { + "Bounded" => { + if values.len() == 2 { + let min = default_or_parse(values[0], DEFAULT_MIN_NURSERY)?; + let max = default_or_parse(values[1], DEFAULT_MAX_NURSERY)?; + Ok(NurserySize::Bounded { min, max }) + } else { + Err("Bounded requires two values".to_string()) + } + } + "ProportionalBounded" => { + if values.len() == 2 { + let min = default_or_parse(values[0], DEFAULT_PROPORTIONAL_MIN_NURSERY)?; + let max = default_or_parse(values[1], DEFAULT_PROPORTIONAL_MAX_NURSERY)?; + Ok(NurserySize::ProportionalBounded { min, max }) + } else { + Err("ProportionalBounded requires two values".to_string()) + } + } + "Fixed" => { + if values.len() == 1 { + let size = values[0] + .parse::() + .map_err(|_| "Invalid size value".to_string())?; + Ok(NurserySize::Fixed(size)) + } else { + Err("Fixed requires one value".to_string()) + } + } + _ => Err("Unknown variant".to_string()), + } } +} - /// Return lower bound of the nursery size (in number of bytes) - pub fn get_min_nursery_bytes(&self) -> usize { - self.nursery.min - } +#[cfg(test)] +mod nursery_size_parsing_tests { + use super::*; + + #[test] + fn test_bounded() { + // Simple case + let result = "Bounded:1,2".parse::().unwrap(); + if let NurserySize::Bounded { min, max } = result { + assert_eq!(min, 1); + assert_eq!(max, 2); + } else { + panic!("Failed: {:?}", result); + } + + // Default min + let result = "Bounded:_,2".parse::().unwrap(); + if let NurserySize::Bounded { min, max } = result { + assert_eq!(min, DEFAULT_MIN_NURSERY); + assert_eq!(max, 2); + } else { + panic!("Failed: {:?}", result); + } + + // Default max + let result = "Bounded:1,_".parse::().unwrap(); + if let NurserySize::Bounded { min, max } = result { + assert_eq!(min, 1); + assert_eq!(max, DEFAULT_MAX_NURSERY); + } else { + panic!("Failed: {:?}", result); + } - /// Return lower bound of the nursery size (in number of pages) - pub fn get_min_nursery_pages(&self) -> usize { - crate::util::conversions::bytes_to_pages_up(self.nursery.min) + // Default both + let result = "Bounded:_,_".parse::().unwrap(); + if let NurserySize::Bounded { min, max } = result { + assert_eq!(min, DEFAULT_MIN_NURSERY); + assert_eq!(max, DEFAULT_MAX_NURSERY); + } else { + panic!("Failed: {:?}", result); + } } - /// Check if the options are set for stress GC. If either stress_factor or analysis_factor is set, - /// we should do stress GC. - pub fn is_stress_test_gc_enabled(&self) -> bool { - *self.stress_factor != DEFAULT_STRESS_FACTOR - || *self.analysis_factor != DEFAULT_STRESS_FACTOR + #[test] + fn test_proportional() { + // Simple case + let result = "ProportionalBounded:0.1,0.8" + .parse::() + .unwrap(); + if let NurserySize::ProportionalBounded { min, max } = result { + assert_eq!(min, 0.1); + assert_eq!(max, 0.8); + } else { + panic!("Failed: {:?}", result); + } + + // Default min + let result = "ProportionalBounded:_,0.8".parse::().unwrap(); + if let NurserySize::ProportionalBounded { min, max } = result { + assert_eq!(min, DEFAULT_PROPORTIONAL_MIN_NURSERY); + assert_eq!(max, 0.8); + } else { + panic!("Failed: {:?}", result); + } + + // Default max + let result = "ProportionalBounded:0.1,_".parse::().unwrap(); + if let NurserySize::ProportionalBounded { min, max } = result { + assert_eq!(min, 0.1); + assert_eq!(max, DEFAULT_PROPORTIONAL_MAX_NURSERY); + } else { + panic!("Failed: {:?}", result); + } + + // Default both + let result = "ProportionalBounded:_,_".parse::().unwrap(); + if let NurserySize::ProportionalBounded { min, max } = result { + assert_eq!(min, DEFAULT_PROPORTIONAL_MIN_NURSERY); + assert_eq!(max, DEFAULT_PROPORTIONAL_MAX_NURSERY); + } else { + panic!("Failed: {:?}", result); + } } } @@ -715,13 +798,14 @@ options! { eager_complete_sweep: bool [env_var: true, command_line: true] [always_valid] = false, /// Should we ignore GCs requested by the user (e.g. java.lang.System.gc)? ignore_system_gc: bool [env_var: true, command_line: true] [always_valid] = false, - /// The nursery size for generational plans. It can be one of Bounded or Fixed. The size for a - /// Bounded nursery only controls the upper bound, whereas the size for a Fixed nursery controls - /// both the upper and lower bounds. The nursery size can be set like 'Fixed:8192', for example, - /// to have a Fixed nursery size of 8192 bytes - // FIXME: This is not a good way to have conflicting options -- we should refactor this - nursery: NurserySize [env_var: true, command_line: true] [|v: &NurserySize| v.min > 0 && v.max.map(|max| max > 0 && max >= v.min).unwrap_or(true)] - = NurserySize { kind: NurseryKind::Bounded, min: DEFAULT_MIN_NURSERY, max: None }, + /// The nursery size for generational plans. It can be one of Bounded, ProportionalBounded or Fixed. + /// The nursery size can be set like 'Fixed:8192', for example, + /// to have a Fixed nursery size of 8192 bytes, or 'ProportionalBounded:0.2,1.0' to have a nursery size + /// between 20% and 100% of the heap size. You can omit lower bound and upper bound to use the default + /// value for bounded nursery by using '_'. For example, 'ProportionalBounded:0.1,_' sets the min nursery + /// to 10% of the heap size while using the default value for max nursery. + nursery: NurserySize [env_var: true, command_line: true] [|v: &NurserySize| v.validate()] + = NurserySize::ProportionalBounded { min: DEFAULT_PROPORTIONAL_MIN_NURSERY, max: DEFAULT_PROPORTIONAL_MAX_NURSERY }, /// Should a major GC be performed when a system GC is required? full_heap_system_gc: bool [env_var: true, command_line: true] [always_valid] = false, /// Should finalization be disabled?