Skip to content

Commit

Permalink
Implement mem balancer (mmtk#724)
Browse files Browse the repository at this point in the history
This PR implements mem balancer (https://dl.acm.org/doi/pdf/10.1145/3563323).

Changes:
* Introduce a trait `GenerationalPlan: Plan`, and move methods that are specific to generational plans to the new trait from the old `trait Plan`.
* Change the return type of `Plan::generational()` to return an option of `&dyn GenerationalPlan`. We can use this to know whether a plan is generational or not. If it is, we can further invoke methods about the generational plan.
* Add `GCTriggerPolicy::on_gc_release()` which is called in the `Release` work packet. A `GCTriggerPolicy` can use this to get stats before we reclaim memory (e.g. to get promoted size).
* Add an implementation of mem balancer to replace the current simple resize policy.
* Add some logging to help debug GC triggering.
  • Loading branch information
qinsoon authored and wenyuzhao committed Mar 20, 2023
1 parent bde5317 commit cc31a27
Show file tree
Hide file tree
Showing 12 changed files with 463 additions and 103 deletions.
6 changes: 3 additions & 3 deletions src/plan/generational/barrier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,21 @@ use crate::MMTK;
use super::gc_work::GenNurseryProcessEdges;
use super::gc_work::ProcessModBuf;
use super::gc_work::ProcessRegionModBuf;
use super::global::Gen;
use super::global::CommonGenPlan;

pub struct GenObjectBarrierSemantics<VM: VMBinding> {
/// MMTk instance
mmtk: &'static MMTK<VM>,
/// Generational plan
gen: &'static Gen<VM>,
gen: &'static CommonGenPlan<VM>,
/// Object modbuf. Contains a list of objects that may contain pointers to the nursery space.
modbuf: VectorQueue<ObjectReference>,
/// Array-copy modbuf. Contains a list of sub-arrays or array slices that may contain pointers to the nursery space.
region_modbuf: VectorQueue<VM::VMMemorySlice>,
}

impl<VM: VMBinding> GenObjectBarrierSemantics<VM> {
pub fn new(mmtk: &'static MMTK<VM>, gen: &'static Gen<VM>) -> Self {
pub fn new(mmtk: &'static MMTK<VM>, gen: &'static CommonGenPlan<VM>) -> Self {
Self {
mmtk,
gen,
Expand Down
33 changes: 20 additions & 13 deletions src/plan/generational/copying/global.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use super::gc_work::GenCopyGCWorkContext;
use super::gc_work::GenCopyNurseryGCWorkContext;
use super::mutator::ALLOCATOR_MAPPING;
use crate::plan::generational::global::Gen;
use crate::plan::generational::global::CommonGenPlan;
use crate::plan::generational::global::GenerationalPlan;
use crate::plan::global::BasePlan;
use crate::plan::global::CommonPlan;
use crate::plan::global::CreateGeneralPlanArgs;
Expand All @@ -27,7 +28,7 @@ use mmtk_macros::PlanTraceObject;
#[derive(PlanTraceObject)]
pub struct GenCopy<VM: VMBinding> {
#[fallback_trace]
pub gen: Gen<VM>,
pub gen: CommonGenPlan<VM>,
pub hi: AtomicBool,
#[trace(CopySemantics::Mature)]
pub copyspace0: CopySpace<VM>,
Expand Down Expand Up @@ -98,7 +99,7 @@ impl<VM: VMBinding> Plan for GenCopy<VM> {
}

fn prepare(&mut self, tls: VMWorkerThread) {
let full_heap = !self.is_current_gc_nursery();
let full_heap = !self.gen.is_current_gc_nursery();
self.gen.prepare(tls);
if full_heap {
self.hi
Expand All @@ -118,7 +119,7 @@ impl<VM: VMBinding> Plan for GenCopy<VM> {
}

fn release(&mut self, tls: VMWorkerThread) {
let full_heap = !self.is_current_gc_nursery();
let full_heap = !self.gen.is_current_gc_nursery();
self.gen.release(tls);
if full_heap {
self.fromspace().release();
Expand All @@ -127,7 +128,7 @@ impl<VM: VMBinding> Plan for GenCopy<VM> {

fn end_of_gc(&mut self, _tls: VMWorkerThread) {
self.gen
.set_next_gc_full_heap(Gen::should_next_gc_be_full_heap(self));
.set_next_gc_full_heap(CommonGenPlan::should_next_gc_be_full_heap(self));
}

fn get_collection_reserved_pages(&self) -> usize {
Expand All @@ -147,10 +148,6 @@ impl<VM: VMBinding> Plan for GenCopy<VM> {
>> 1
}

fn get_mature_physical_pages_available(&self) -> usize {
self.tospace().available_physical_pages()
}

fn base(&self) -> &BasePlan<VM> {
&self.gen.common.base
}
Expand All @@ -159,12 +156,22 @@ impl<VM: VMBinding> Plan for GenCopy<VM> {
&self.gen.common
}

fn generational(&self) -> &Gen<VM> {
fn generational(&self) -> Option<&dyn GenerationalPlan<VM = Self::VM>> {
Some(self)
}
}

impl<VM: VMBinding> GenerationalPlan for GenCopy<VM> {
fn common_gen(&self) -> &CommonGenPlan<Self::VM> {
&self.gen
}

fn is_current_gc_nursery(&self) -> bool {
!self.gen.gc_full_heap.load(Ordering::SeqCst)
fn get_mature_physical_pages_available(&self) -> usize {
self.tospace().available_physical_pages()
}

fn get_mature_reserved_pages(&self) -> usize {
self.tospace().reserved_pages()
}
}

Expand All @@ -187,7 +194,7 @@ impl<VM: VMBinding> GenCopy<VM> {
);

let res = GenCopy {
gen: Gen::new(plan_args),
gen: CommonGenPlan::new(plan_args),
hi: AtomicBool::new(false),
copyspace0,
copyspace1,
Expand Down
22 changes: 17 additions & 5 deletions src/plan/generational/gc_work.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use atomic::Ordering;

use crate::plan::generational::global::Gen;
use crate::plan::generational::global::CommonGenPlan;
use crate::policy::space::Space;
use crate::scheduler::{gc_work::*, GCWork, GCWorker};
use crate::util::ObjectReference;
Expand All @@ -14,7 +14,7 @@ use std::ops::{Deref, DerefMut};
/// [`crate::scheduler::gc_work::SFTProcessEdges`]. If a plan uses `SFTProcessEdges`,
/// it does not need to use this type.
pub struct GenNurseryProcessEdges<VM: VMBinding> {
gen: &'static Gen<VM>,
gen: &'static CommonGenPlan<VM>,
base: ProcessEdgesBase<VM>,
}

Expand All @@ -24,7 +24,7 @@ impl<VM: VMBinding> ProcessEdgesWork for GenNurseryProcessEdges<VM> {

fn new(edges: Vec<EdgeOf<Self>>, roots: bool, mmtk: &'static MMTK<VM>) -> Self {
let base = ProcessEdgesBase::new(edges, roots, mmtk);
let gen = base.plan().generational();
let gen = base.plan().generational().unwrap().common_gen();
Self { gen, base }
}
#[inline]
Expand Down Expand Up @@ -95,7 +95,13 @@ impl<E: ProcessEdgesWork> GCWork<E::VM> for ProcessModBuf<E> {
);
}
// scan modbuf only if the current GC is a nursery GC
if mmtk.plan.is_current_gc_nursery() {
if mmtk
.plan
.generational()
.unwrap()
.common_gen()
.is_current_gc_nursery()
{
// Scan objects in the modbuf and forward pointers
let modbuf = std::mem::take(&mut self.modbuf);
GCWork::do_work(
Expand Down Expand Up @@ -129,7 +135,13 @@ impl<E: ProcessEdgesWork> GCWork<E::VM> for ProcessRegionModBuf<E> {
#[inline(always)]
fn do_work(&mut self, worker: &mut GCWorker<E::VM>, mmtk: &'static MMTK<E::VM>) {
// Scan modbuf only if the current GC is a nursery GC
if mmtk.plan.is_current_gc_nursery() {
if mmtk
.plan
.generational()
.unwrap()
.common_gen()
.is_current_gc_nursery()
{
// Collect all the entries in all the slices
let mut edges = vec![];
for slice in &self.modbuf {
Expand Down
72 changes: 58 additions & 14 deletions src/plan/generational/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use crate::plan::Plan;
use crate::policy::copyspace::CopySpace;
use crate::policy::space::Space;
use crate::scheduler::*;
use crate::util::conversions;
use crate::util::copy::CopySemantics;
use crate::util::heap::VMRequest;
use crate::util::metadata::side_metadata::SideMetadataSanity;
Expand All @@ -22,7 +21,7 @@ use mmtk_macros::PlanTraceObject;
/// Common implementation for generational plans. Each generational plan
/// should include this type, and forward calls to it where possible.
#[derive(PlanTraceObject)]
pub struct Gen<VM: VMBinding> {
pub struct CommonGenPlan<VM: VMBinding> {
/// The nursery space.
#[trace(CopySemantics::PromoteToMature)]
pub nursery: CopySpace<VM>,
Expand All @@ -36,21 +35,21 @@ pub struct Gen<VM: VMBinding> {
pub full_heap_gc_count: Arc<Mutex<EventCounter>>,
}

impl<VM: VMBinding> Gen<VM> {
impl<VM: VMBinding> CommonGenPlan<VM> {
pub fn new(mut args: CreateSpecificPlanArgs<VM>) -> Self {
let nursery = CopySpace::new(
args.get_space_args(
"nursery",
true,
VMRequest::fixed_extent(args.global_args.options.get_max_nursery(), false),
VMRequest::fixed_extent(args.global_args.options.get_max_nursery_bytes(), false),
),
true,
);
let common = CommonPlan::new(args);

let full_heap_gc_count = common.base.stats.new_event_counter("majorGC", true, true);

Gen {
CommonGenPlan {
nursery,
common,
gc_full_heap: AtomicBool::default(),
Expand Down Expand Up @@ -97,28 +96,35 @@ impl<VM: VMBinding> Gen<VM> {
///
/// Returns `true` if the nursery has grown to the extent that it may not be able to be copied
/// into the mature space.
fn virtual_memory_exhausted<P: Plan>(&self, plan: &P) -> bool {
fn virtual_memory_exhausted(plan: &dyn GenerationalPlan<VM = VM>) -> bool {
((plan.get_collection_reserved_pages() as f64
* VM::VMObjectModel::VM_WORST_CASE_COPY_EXPANSION) as usize)
> plan.get_mature_physical_pages_available()
}

/// Check if we need a GC based on the nursery space usage. This method may mark
/// the following GC as a full heap GC.
pub fn collection_required<P: Plan>(
pub fn collection_required<P: Plan<VM = VM>>(
&self,
plan: &P,
space_full: bool,
space: Option<&dyn Space<VM>>,
) -> bool {
let nursery_full = self.nursery.reserved_pages()
>= (conversions::bytes_to_pages_up(self.common.base.options.get_max_nursery()));
let cur_nursery = self.nursery.reserved_pages();
let max_nursery = self.common.base.options.get_max_nursery_pages();
let nursery_full = cur_nursery >= max_nursery;
trace!(
"nursery_full = {:?} (nursery = {}, max_nursery = {})",
nursery_full,
cur_nursery,
max_nursery,
);

if nursery_full {
return true;
}

if self.virtual_memory_exhausted(plan) {
if Self::virtual_memory_exhausted(plan.generational().unwrap()) {
return true;
}

Expand Down Expand Up @@ -146,11 +152,12 @@ impl<VM: VMBinding> Gen<VM> {

/// Check if we should do a full heap GC. It returns true if we should have a full heap GC.
/// It also sets gc_full_heap based on the result.
pub fn requires_full_heap_collection<P: Plan>(&self, plan: &P) -> bool {
pub fn requires_full_heap_collection<P: Plan<VM = VM>>(&self, plan: &P) -> bool {
// Allow the same 'true' block for if-else.
// The conditions are complex, and it is easier to read if we put them to separate if blocks.
#[allow(clippy::if_same_then_else, clippy::needless_bool)]
let is_full_heap = if crate::plan::generational::FULL_NURSERY_GC {
trace!("full heap: forced full heap");
// For barrier overhead measurements, we always do full gc in nursery collections.
true
} else if self
Expand All @@ -160,6 +167,7 @@ impl<VM: VMBinding> Gen<VM> {
.load(Ordering::SeqCst)
&& *self.common.base.options.full_heap_system_gc
{
trace!("full heap: user triggered");
// User triggered collection, and we force full heap for user triggered collection
true
} else if self.next_gc_full_heap.load(Ordering::SeqCst)
Expand All @@ -170,9 +178,18 @@ impl<VM: VMBinding> Gen<VM> {
.load(Ordering::SeqCst)
> 1
{
trace!(
"full heap: next_gc_full_heap = {}, cur_collection_attempts = {}",
self.next_gc_full_heap.load(Ordering::SeqCst),
self.common
.base
.cur_collection_attempts
.load(Ordering::SeqCst)
);
// Forces full heap collection
true
} else if self.virtual_memory_exhausted(plan) {
} else if Self::virtual_memory_exhausted(plan.generational().unwrap()) {
trace!("full heap: virtual memory exhausted");
true
} else {
// We use an Appel-style nursery. The default GC (even for a "heap-full" collection)
Expand Down Expand Up @@ -250,8 +267,16 @@ impl<VM: VMBinding> Gen<VM> {
/// [`get_available_pages`](crate::plan::Plan::get_available_pages)
/// whose value depends on which spaces have been released.
pub fn should_next_gc_be_full_heap(plan: &dyn Plan<VM = VM>) -> bool {
plan.get_available_pages()
< conversions::bytes_to_pages_up(plan.base().options.get_min_nursery())
let available = plan.get_available_pages();
let min_nursery = plan.base().options.get_min_nursery_pages();
let next_gc_full_heap = available < min_nursery;
trace!(
"next gc will be full heap? {}, availabe pages = {}, min nursery = {}",
next_gc_full_heap,
available,
min_nursery
);
next_gc_full_heap
}

/// Set next_gc_full_heap to the given value.
Expand All @@ -272,3 +297,22 @@ impl<VM: VMBinding> Gen<VM> {
self.nursery.reserved_pages() + self.common.get_used_pages()
}
}

/// This trait include methods that are specific to generational plans.
pub trait GenerationalPlan: Plan {
/// Return the common generational implementation [`crate::plan::generational::global::CommonGenPlan`].
fn common_gen(&self) -> &CommonGenPlan<Self::VM>;

/// Return the number of pages available for allocation into the mature space.
fn get_mature_physical_pages_available(&self) -> usize;

/// Return the number of used pages in the mature space.
fn get_mature_reserved_pages(&self) -> usize;
}

/// Is current GC only collecting objects allocated since last GC? This method can be called
/// with any plan (generational or not). For non generational plans, it will always return false.
pub fn is_nursery_gc<VM: VMBinding>(plan: &dyn Plan<VM = VM>) -> bool {
plan.generational()
.map_or(false, |plan| plan.common_gen().is_current_gc_nursery())
}
Loading

0 comments on commit cc31a27

Please sign in to comment.