Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heap traversal #1174

Merged
merged 36 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b0d297d
WIP: enumerate objects and regions
wks Jul 24, 2024
c69a68b
WIP: linear scanning metadata
wks Jul 24, 2024
a837987
Refactor iterate_meta_bits
wks Jul 29, 2024
b1b975a
Merge branch 'master' into feature/each-object
wks Jul 30, 2024
7d3f79d
Fix warning
wks Jul 30, 2024
acaf942
Benchmarks
wks Jul 31, 2024
553f210
Merge branch 'fix/bulk-metadata-visit-simple' into feature/each-object
wks Aug 1, 2024
f4a08b3
Remove ByteWordRange
wks Aug 1, 2024
a51cdc4
Fix clippy warnings
wks Aug 1, 2024
b1486e3
Formatting
wks Aug 1, 2024
ab28a79
Move iterate_meta_bits to ranges::break_bit_range
wks Aug 1, 2024
e2bafd5
Add tests and fix bugs
wks Aug 1, 2024
c3c3dfa
Just use u8 for the type of bit offset.
wks Aug 2, 2024
19a366f
Merge branch 'fix/bulk-metadata-visit-simple' into feature/each-object
wks Aug 2, 2024
3a4747c
Fix and comment
wks Aug 2, 2024
765da3b
Formatting
wks Aug 2, 2024
a73061c
Comments
wks Aug 5, 2024
5f1e99e
Merge branch 'fix/bulk-metadata-visit-simple' into feature/each-object
wks Aug 5, 2024
419655f
Minor changes
wks Aug 5, 2024
6eb3073
Mock test for heap traversal
wks Aug 5, 2024
f3c80af
Add benchmark for bitmap scanning
wks Aug 5, 2024
d994f6e
Merge branch 'master' into fix/bulk-metadata-visit-simple
wks Aug 5, 2024
323d39e
Revert criterion version update
wks Aug 6, 2024
95cd6df
Fix comments
wks Aug 6, 2024
32b4ab8
Merge branch 'fix/bulk-metadata-visit-simple' into feature/each-object
wks Aug 6, 2024
8b91a21
Support VMSpace
wks Aug 6, 2024
70e8933
Renamed to Space::enumerate_object_coarse
wks Aug 6, 2024
4f6d1bd
Revert `Region::as_range()`.
wks Aug 6, 2024
e1669d2
Merge branch 'master' into feature/each-object
wks Aug 7, 2024
38edbc9
Merge branch 'master' into feature/each-object
wks Aug 7, 2024
557e7c4
Remove the "bench" feature
wks Aug 7, 2024
ec194aa
Comments
wks Aug 7, 2024
ce3c723
Rename back to Space::enumerate_objects
wks Aug 8, 2024
81a9fab
Use vo_bit::get_object_ref_for_vo_addr
wks Aug 8, 2024
d11aa54
Update comments of MMTK::enumerate_objects
wks Aug 8, 2024
7130211
Rename remaining enumerate_objects_coarse
wks Aug 8, 2024
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ sysinfo = "0.30.9"
[dev-dependencies]
paste = "1.0.8"
rand = "0.8.5"
rand_chacha = "0.3.1"
criterion = "0.4"

[build-dependencies]
Expand Down
87 changes: 87 additions & 0 deletions benches/regular_bench/bulk_meta/bscan.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! Benchmarks for scanning side metadata for non-zero bits.

use criterion::Criterion;
use mmtk::util::{
constants::LOG_BITS_IN_WORD, test_private::scan_non_zero_bits_in_metadata_bytes, Address,
};
use rand::{seq::IteratorRandom, SeedableRng};
use rand_chacha::ChaCha8Rng;

fn allocate_aligned(size: usize) -> Address {
let ptr = unsafe {
std::alloc::alloc_zeroed(std::alloc::Layout::from_size_align(size, size).unwrap())
};
Address::from_mut_ptr(ptr)
}

const BLOCK_BYTES: usize = 32768usize; // Match an Immix block size.

// Asssume one-bit-per-word metadata (matching VO bits).
const BLOCK_META_BYTES: usize = BLOCK_BYTES >> LOG_BITS_IN_WORD;

/// Set this many distinct bits in the bitmap.
const NUM_OBJECTS: usize = 200;

/// Get a deterministic seeded Rng.
fn get_rng() -> ChaCha8Rng {
// Create an Rng from a seed and an explicit Rng type.
// Not secure at all, but completely deterministic and reproducible.
// The following seed is read from /dev/random
const SEED64: u64 = 0x4050cb1b5ab26c70;
ChaCha8Rng::seed_from_u64(SEED64)
}

/// A bitmap, with known location of each bit for assertion.
struct PreparedBitmap {
start: Address,
end: Address,
set_bits: Vec<(Address, u8)>,
}

/// Make a bitmap of the desired size and set bits.
fn make_standard_bitmap() -> PreparedBitmap {
let start = allocate_aligned(BLOCK_META_BYTES);
let end = start + BLOCK_META_BYTES;
let mut rng = get_rng();

let mut set_bits = (0..(BLOCK_BYTES >> LOG_BITS_IN_WORD))
.choose_multiple(&mut rng, NUM_OBJECTS)
.iter()
.map(|total_bit_offset| {
let word_offset = total_bit_offset >> LOG_BITS_IN_WORD;
let bit_offset = total_bit_offset & ((1 << LOG_BITS_IN_WORD) - 1);
(start + (word_offset << LOG_BITS_IN_WORD), bit_offset as u8)
})
.collect::<Vec<_>>();

set_bits.sort();

for (addr, bit) in set_bits.iter() {
let word = unsafe { addr.load::<usize>() };
let new_word = word | (1 << bit);
unsafe { addr.store::<usize>(new_word) };
}

PreparedBitmap {
start,
end,
set_bits,
}
}

pub fn bench(c: &mut Criterion) {
c.bench_function("bscan_block", |b| {
let bitmap = make_standard_bitmap();
let mut holder: Vec<(Address, u8)> = Vec::with_capacity(NUM_OBJECTS);

b.iter(|| {
holder.clear();
scan_non_zero_bits_in_metadata_bytes(bitmap.start, bitmap.end, &mut |addr, shift| {
holder.push((addr, shift));
});
});

assert_eq!(holder.len(), NUM_OBJECTS);
assert_eq!(holder, bitmap.set_bits);
});
}
2 changes: 2 additions & 0 deletions benches/regular_bench/bulk_meta/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
pub mod bscan;
pub mod bzero_bset;

pub use criterion::Criterion;

pub fn bench(c: &mut Criterion) {
bscan::bench(c);
bzero_bset::bench(c);
}
44 changes: 44 additions & 0 deletions src/mmtk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use crate::plan::Plan;
use crate::policy::sft_map::{create_sft_map, SFTMap};
use crate::scheduler::GCWorkScheduler;

#[cfg(feature = "vo_bit")]
use crate::util::address::ObjectReference;
#[cfg(feature = "analysis")]
use crate::util::analysis::AnalysisManager;
use crate::util::finalizable_processor::FinalizableProcessor;
Expand Down Expand Up @@ -467,4 +469,46 @@ impl<VM: VMBinding> MMTK<VM> {
pub fn get_options(&self) -> &Options {
&self.options
}

/// Enumerate objects in all spaces in this MMTK instance.
///
/// The call-back function `f` is called for every object that has the valid object bit (VO
/// bit), i.e. objects that are allocated in the heap of this MMTK instance, but has not been
/// reclaimed, yet.
///
/// # Interaction with allocation and GC
///
/// This function does not mutate the heap. It is safe if multiple threads execute this
/// function concurrently during mutator time.
///
/// This function will visit all objects that have been allocated at the time when this function
/// is called. But if new objects are allocated while this function is being executed, this
/// function may or may not visit objects allocated after this function started.
///
/// Also note that when this function visits an object, it only guarantees that its VO bit must
/// have been set. It is not guaranteed if the object has been "fully initialized" in the sense
/// of the programming language the VM is implementing. For example, the object header and the
/// type information may not have been written.
///
/// It has *undefined behavior* if GC happens while this function is being executed. The VM
/// binding must ensure GC does not start while executing this function. One way to prevent GC
/// from starting is not letting the current thread yield for GC.
///
/// Some programming languages may provide an API that allows the user to allocate objects and
/// trigger GC while enumerating objects. The VM binding may use the callback of this function
/// to save all visited objects in an array and let the user visit the array after this function
/// returned.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how it would be helpful to save the visited objects and visit them later. If the GC is happening, the visited objects may be reclaimed during this call, and the saved object references may become invalid.

As this function uses VO bit (which is changed during GC and allocation), and the function does not block GC and does not block allocation, it should be undefined behavior if GC or allocation happens at the same time. We cannot guarantee anything if GC/allocation is happenning. We may call the closure for an object that seems valid, but when the closure visits the object, the object may be reclaimed. We may visit objects that are copied (but their VO bits are not yet cleared). We may miss objects that are just allocated.

Copy link
Collaborator Author

@wks wks Aug 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how it would be helpful to save the visited objects and visit them later. If the GC is happening, the visited objects may be reclaimed during this call, and the saved object references may become invalid.

The VM binding needs to treat the saved list of object references as roots. In the way, if GC happens, those objects will be guaranteed to live through the next GC.

The list may contain the last reference to some objects because the object may be unlinked from their original parents after the last GC. That's expected. Such heap traversal APIs are allowed to pick up objects that have become unreachable, finalized, etc., and the VM binding needs to be aware of that. If such unreachable objects points to other objects, their outgoing edges will still remain valid because (1) if GC has not happened, their edges remain valid, and (2) if GC happens, those objects are rooted and will be traced and forwarded.

If the VM can update the roots (in the same way updating JNI handles or other native roots), the saved list can be updated if a copying GC happens. Alternatively, if the VM only intends to support plans that support pinning, it can treat the list as pinning roots, and those objects will not be moved while iterating through the list.

I have tested it on Ruby by manually triggering a GC between saving the list (as a pinning root) and iterating through the list, and also triggering GC after visiting every few objects. It works.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you said does not contradict with what I said. I suggested that 'it should be undefined behavior if GC or allocation happens at the same time (during the enumerating function call)'. After the enumerating function, the binding can do whatever they need to make an 'illusion' that the user is enumerating objects during a GC. But this function itself is not allowed during GC.

Suggested change
/// trigger GC while enumerating objects. The VM binding may use the callback of this function
/// to save all visited objects in an array and let the user visit the array after this function
/// returned.
/// trigger GC while enumerating objects. The VM binding may use the callback of this function
/// to save all visited objects in an array outside GCs and let the user visit the array after this function
/// returned. The binding needs to carefully keep the object references in the array alive and updated properly if
/// a GC happens.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it should have undefined behavior if GC happens.

I thought allocation should be benign because allocation strictly adds VO bits but does not remove VO bits. But after thinking about it, I find that it is easier to just specify that it has undefined behavior if allocation happens concurrently, too. Reasons include

  • We currently accesses the VO bits at different granularities, including bit, byte, word, etc. The interaction of such memory accesses in a multi-threaded environment is not well-defined in the C++11 or Rust memory model. So it is hard to reason what the reader will see (or whether it has undefined behavior) if the read and the write happens concurrently.
  • In a multi-threaded program, different threads may not agree upon a global total order in which different VO bits are set. (Thread 1 may see bit X is set before bit Y, but Thread2 may see bit Y is set before bit X.) So "all bits that have been set when enumerate_objects is called" is not precisely defined unless we make all threads stop, or at least synchornize with the current thread.

It is OK for Ruby because we currently only supports a single Ractor (in which only one thread is active at any time). For OpenJDK, it will require a VMOperation to stop the world before enumerating objects.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the comment. I made it clear that it is a data race if either allocation or GC happens concurrently with enumerate_objects. I also added some comments on keeping the saved object references in roots if the VM binding implements an API that allows allocation while enumerating objects.

#[cfg(feature = "vo_bit")]
pub fn enumerate_objects<F>(&self, f: F)
where
F: FnMut(ObjectReference),
{
use crate::util::object_enum;

let mut enumerator = object_enum::ClosureObjectEnumerator::<_, VM>::new(f);
let plan = self.get_plan();
plan.for_each_space(&mut |space| {
space.enumerate_objects_coarse(&mut enumerator);
})
}
}
7 changes: 6 additions & 1 deletion src/policy/copyspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ use crate::policy::sft::SFT;
use crate::policy::space::{CommonSpace, Space};
use crate::scheduler::GCWorker;
use crate::util::alloc::allocator::AllocatorContext;
use crate::util::copy::*;
use crate::util::heap::{MonotonePageResource, PageResource};
use crate::util::metadata::{extract_side_metadata, MetadataSpec};
use crate::util::object_enum::ObjectEnumerator;
use crate::util::object_forwarding;
use crate::util::{copy::*, object_enum};
use crate::util::{Address, ObjectReference};
use crate::vm::*;
use libc::{mprotect, PROT_EXEC, PROT_NONE, PROT_READ, PROT_WRITE};
Expand Down Expand Up @@ -133,6 +134,10 @@ impl<VM: VMBinding> Space<VM> for CopySpace<VM> {
fn set_copy_for_sft_trace(&mut self, semantics: Option<CopySemantics>) {
self.common.copy = semantics;
}

fn enumerate_objects_coarse(&self, enumerator: &mut dyn ObjectEnumerator) {
object_enum::enumerate_blocks_from_monotonic_page_resource(enumerator, &self.pr);
}
}

impl<VM: VMBinding> crate::policy::gc_work::PolicyTraceObject<VM> for CopySpace<VM> {
Expand Down
7 changes: 7 additions & 0 deletions src/policy/immix/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::util::metadata::side_metadata::{MetadataByteArrayRef, SideMetadataSpe
use crate::util::metadata::vo_bit;
#[cfg(feature = "object_pinning")]
use crate::util::metadata::MetadataSpec;
use crate::util::object_enum::BlockMayHaveObjects;
use crate::util::Address;
use crate::vm::*;
use std::sync::atomic::Ordering;
Expand Down Expand Up @@ -86,6 +87,12 @@ impl Region for Block {
}
}

impl BlockMayHaveObjects for Block {
fn may_have_objects(&self) -> bool {
self.get_state() != BlockState::Unallocated
}
}

impl Block {
/// Log pages in block
pub const LOG_PAGES: usize = Self::LOG_BYTES - LOG_BYTES_IN_PAGE as usize;
Expand Down
7 changes: 6 additions & 1 deletion src/policy/immix/immixspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ use crate::util::metadata::side_metadata::SideMetadataSpec;
#[cfg(feature = "vo_bit")]
use crate::util::metadata::vo_bit;
use crate::util::metadata::{self, MetadataSpec};
use crate::util::object_enum::ObjectEnumerator;
use crate::util::object_forwarding;
use crate::util::{copy::*, epilogue};
use crate::util::{copy::*, epilogue, object_enum};
use crate::util::{Address, ObjectReference};
use crate::vm::*;
use crate::{
Expand Down Expand Up @@ -189,6 +190,10 @@ impl<VM: VMBinding> Space<VM> for ImmixSpace<VM> {
fn set_copy_for_sft_trace(&mut self, _semantics: Option<CopySemantics>) {
panic!("We do not use SFT to trace objects for Immix. set_copy_context() cannot be used.")
}

fn enumerate_objects_coarse(&self, enumerator: &mut dyn ObjectEnumerator) {
object_enum::enumerate_blocks_from_chunk_map::<Block>(enumerator, &self.chunk_map);
}
}

impl<VM: VMBinding> crate::policy::gc_work::PolicyTraceObject<VM> for ImmixSpace<VM> {
Expand Down
5 changes: 5 additions & 0 deletions src/policy/immortalspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::util::address::Address;
use crate::util::heap::{MonotonePageResource, PageResource};
use crate::util::metadata::mark_bit::MarkState;

use crate::util::object_enum::{self, ObjectEnumerator};
use crate::util::{metadata, ObjectReference};

use crate::plan::{ObjectQueue, VectorObjectQueue};
Expand Down Expand Up @@ -112,6 +113,10 @@ impl<VM: VMBinding> Space<VM> for ImmortalSpace<VM> {
fn release_multiple_pages(&mut self, _start: Address) {
panic!("immortalspace only releases pages enmasse")
}

fn enumerate_objects_coarse(&self, enumerator: &mut dyn ObjectEnumerator) {
object_enum::enumerate_blocks_from_monotonic_page_resource(enumerator, &self.pr);
}
}

use crate::scheduler::GCWorker;
Expand Down
5 changes: 5 additions & 0 deletions src/policy/largeobjectspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::policy::space::{CommonSpace, Space};
use crate::util::constants::BYTES_IN_PAGE;
use crate::util::heap::{FreeListPageResource, PageResource};
use crate::util::metadata;
use crate::util::object_enum::ObjectEnumerator;
use crate::util::opaque_pointer::*;
use crate::util::treadmill::TreadMill;
use crate::util::{Address, ObjectReference};
Expand Down Expand Up @@ -175,6 +176,10 @@ impl<VM: VMBinding> Space<VM> for LargeObjectSpace<VM> {
fn release_multiple_pages(&mut self, start: Address) {
self.pr.release_pages(start);
}

fn enumerate_objects_coarse(&self, enumerator: &mut dyn ObjectEnumerator) {
self.treadmill.enumerate_objects_coarse(enumerator);
}
}

use crate::scheduler::GCWorker;
Expand Down
5 changes: 5 additions & 0 deletions src/policy/lockfreeimmortalspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::util::heap::VMRequest;
use crate::util::memory::MmapStrategy;
use crate::util::metadata::side_metadata::SideMetadataContext;
use crate::util::metadata::side_metadata::SideMetadataSanity;
use crate::util::object_enum::ObjectEnumerator;
use crate::util::opaque_pointer::*;
use crate::util::ObjectReference;
use crate::vm::VMBinding;
Expand Down Expand Up @@ -166,6 +167,10 @@ impl<VM: VMBinding> Space<VM> for LockFreeImmortalSpace<VM> {
side_metadata_sanity_checker
.verify_metadata_context(std::any::type_name::<Self>(), &self.metadata)
}

fn enumerate_objects_coarse(&self, enumerator: &mut dyn ObjectEnumerator) {
enumerator.visit_address_range(self.start, self.start + self.total_bytes);
}
}

use crate::plan::{ObjectQueue, VectorObjectQueue};
Expand Down
5 changes: 5 additions & 0 deletions src/policy/markcompactspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::util::constants::LOG_BYTES_IN_WORD;
use crate::util::copy::CopySemantics;
use crate::util::heap::{MonotonePageResource, PageResource};
use crate::util::metadata::{extract_side_metadata, vo_bit};
use crate::util::object_enum::{self, ObjectEnumerator};
use crate::util::{Address, ObjectReference};
use crate::{vm::*, ObjectQueue};
use atomic::Ordering;
Expand Down Expand Up @@ -131,6 +132,10 @@ impl<VM: VMBinding> Space<VM> for MarkCompactSpace<VM> {
fn release_multiple_pages(&mut self, _start: Address) {
panic!("markcompactspace only releases pages enmasse")
}

fn enumerate_objects_coarse(&self, enumerator: &mut dyn ObjectEnumerator) {
object_enum::enumerate_blocks_from_monotonic_page_resource(enumerator, &self.pr);
}
}

impl<VM: VMBinding> crate::policy::gc_work::PolicyTraceObject<VM> for MarkCompactSpace<VM> {
Expand Down
5 changes: 5 additions & 0 deletions src/policy/marksweepspace/malloc_ms/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::util::metadata::side_metadata::{
SideMetadataContext, SideMetadataSanity, SideMetadataSpec,
};
use crate::util::metadata::MetadataSpec;
use crate::util::object_enum::ObjectEnumerator;
use crate::util::opaque_pointer::*;
use crate::util::Address;
use crate::util::ObjectReference;
Expand Down Expand Up @@ -229,6 +230,10 @@ impl<VM: VMBinding> Space<VM> for MallocSpace<VM> {
side_metadata_sanity_checker
.verify_metadata_context(std::any::type_name::<Self>(), &self.metadata)
}

fn enumerate_objects_coarse(&self, _enumerator: &mut dyn ObjectEnumerator) {
unimplemented!()
}
}

use crate::scheduler::GCWorker;
Expand Down
7 changes: 7 additions & 0 deletions src/policy/marksweepspace/native_ms/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use super::MarkSweepSpace;
use crate::util::constants::LOG_BYTES_IN_PAGE;
use crate::util::heap::chunk_map::*;
use crate::util::linear_scan::Region;
use crate::util::object_enum::BlockMayHaveObjects;
use crate::vm::ObjectModel;
use crate::{
util::{
Expand Down Expand Up @@ -48,6 +49,12 @@ impl Region for Block {
}
}

impl BlockMayHaveObjects for Block {
fn may_have_objects(&self) -> bool {
self.get_state() != BlockState::Unallocated
}
}

impl Block {
/// Log pages in block
pub const LOG_PAGES: usize = Self::LOG_BYTES - LOG_BYTES_IN_PAGE as usize;
Expand Down
5 changes: 5 additions & 0 deletions src/policy/marksweepspace/native_ms/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::{
epilogue,
heap::{BlockPageResource, PageResource},
metadata::{self, side_metadata::SideMetadataSpec, MetadataSpec},
object_enum::{self, ObjectEnumerator},
ObjectReference,
},
vm::{ActivePlan, VMBinding},
Expand Down Expand Up @@ -247,6 +248,10 @@ impl<VM: VMBinding> Space<VM> for MarkSweepSpace<VM> {
fn release_multiple_pages(&mut self, _start: crate::util::Address) {
todo!()
}

fn enumerate_objects_coarse(&self, enumerator: &mut dyn ObjectEnumerator) {
object_enum::enumerate_blocks_from_chunk_map::<Block>(enumerator, &self.chunk_map);
}
}

impl<VM: VMBinding> crate::policy::gc_work::PolicyTraceObject<VM> for MarkSweepSpace<VM> {
Expand Down
Loading
Loading