Description
Each space has prepare
, release
and optionally end_of_gc
methods. But their arguments differ.
Prepare:
CopySpace
:pub fn prepare(&self, from_space: bool)
MallocSpace
:pub fn prepare(&mut self, full_heap: bool)
MarkCompactSpace
:pub fn prepare(&self) {}
ImmixSpace
:pub fn prepare(&mut self, major_gc: bool, plan_stats: StatsForDefrag)
LargeObjectSpace
:pub fn prepare(&mut self, full_heap: bool)
ImmortalSpace
:pub fn prepare(&mut self)
VMSpace
:pub fn prepare(&mut self)
The CopySpace
sets its self.from_space
flag inside prepare(from_space)
.
Other spaces usually reset their metadata or set their internal states in prepare
. Different spaces need different information to do so. For example, some spaces are sensitive to whether the current GC is full-heap. ImmixSpace, specifically, will need to determine the available_clean_pages_for_defrag
using statistics from the Plan (which can be Immix
, GenImmix
or StickyImmix
). Interestingly, the decision of whether to do defrag GC is made in schedule_immix_full_heap_collection
which is called in the ScheduleCollection
work packet.
It is annoying that different spaces need different information from the Plan. A PR #1315 attempted to "unify" the prepare/release/end_of_gc
methods of different spaces. But it only made the arguments dynamically typed.
Similar is true for release
. LargeObjectSpace
and ImmixSpace
take an extra full_heap: bool
parameter, while other spaces only needs &self
.
And end_of_gc
is only needed for ImmixSpace
, MarkSweepSpace
.
What should we do?
I am not advocating for making changes soon. We will probably do nothing for now, but it's interesting to look at alternatives.
Do nothing
By doing nothing, we admit that each space needs different information in prepare
and release
, and may or may not need end_of_gc
. Then each Plan has the responsibility to know exactly what their spaces need and provide precisely what information needed to do prepare/release/end_of_gc
.
The good part of this is that the information is precise. Plans and policies have maximum and the most precise control of what information to pass between the plan and the policy.
The bad part is of course being a bit complicated.
Unify those functions
An argument for unifying those functions is that plan is a configuration/composition of spaces. If a plan has multiple spaces, it should give each space a chance to prepare. The good part of this is that it makes the system uniform.
But there are two questions remain to be answered.
- For SemiSpace and GenCopy, the action of "swapping the roles of from/to spaces" is not a per-space decision, but a decision made at the plan level. This somewhat conflicts with the idea of "plan is a configuration/composition of spaces" because there does exist code in the plan, making it impossible to, for example, write a plan as a YAML file.
- What information does all the spaces need? It is hard to work out a union of all information needed by all spaces, at present and in the future. It may be tempting to simply give spaces access to the
Plan
, or&dyn Plan
. But each Plan has specific information, too. In the end, each space may still need to downcast it to some&dyn SomePlanTrait
such as&dyn AbstractImmixBasedPlan
. Although it is slightly better than&dyn Any
, it's still a bit ugly.
Another argument for unifying those functions is that the composition of plans can be dynamic.
For example, with the introduction of #1308, the CommonSpace
may have one of three candidates (ImmixSpace
, MarkSweepSpace
or ImmortalSpace
) as the non-moving space. Some of them needs prepare/release/end_of_gc
, while others don't. Some need more arguments, while others need fewer. Currently #1308 uses if-else or conditional compilation attributes to control the behaviors, and we already see many decisions become more complicated, including the plan-level prepare/release/end_of_gc and the prepare/release of mutators. A refactored version https://github.com/qinsoon/mmtk-core/compare/nonmoving-space-options...wks:mmtk-core:feature/nonmoving-space-options-trait?expand=1 attempted to use the visitor pattern to remove the if-else, but it assumes that we only needs one parameter, full_heap: bool
, for the prepare
method, which is not 100% general. Ideally, each space should know how to prepare itself.
Separating plan-level prepare and space-level prepare.
We may separate operations such as swapping the role of from/to spaces and the operations such as clearing side metadata.
By doing so, the Prepare
work packet may first call mmtk.plan.prepare()
, and then directly go through each space of a plan:
impl<C: GCWorkContext> GCWork<C::VM> for Prepare<C> {
fn do_work(&mut self, worker: &mut GCWorker<C::VM>, mmtk: &'static MMTK<C::VM>) {
trace!("Prepare Global");
let plan_mut: &mut C::PlanType = unsafe { &mut *(self.plan as *const _ as *mut _) }; // Fix me.
plan_mut.prepare(worker.tls);
self.plan.for_each_space(|space| { // Defined in the `HasSpaces` trait, generated by #[derive(HasSpaces)]
space.prepare(plan);
});
This will be maximally general.
For SemiSpace, it can let CopySpace define a separate fn set_is_from_space(&self, from_space: bool)
method, and Semispace;:prepare
will call it on both copyspace0
and copyspace1
. Other plans probably do nothing in its own Plan::prepare
. They doesn't need to delegate to the prepare
methods of its spaces. The Prepare
work packet will do it.
Then the Space.prepare
will have access to the Plan
. If a space is sensitive to whether the current GC is full-heap, it calls
let is_full_heap = !plan.generational.is_some_and(|gen| gen.is_current_gc_nursery());
And all statistics will be available from the plan.
It seems to be a general solution, although it needs quite a lot of refactoring.