Skip to content

Commit

Permalink
Fix various GC bugs and improve GC performance
Browse files Browse the repository at this point in the history
This fixes various Immix related bugs and makes some improvements to the
collector's performance.

Bug fixes
---------

The following bugs have been fixed:

1. Evacuating required one or more fragmented blocks, which were set
   based on histogram statistics. These statistics were only populated
   when there were one or more fragmented blocks. This resulted in
   evacuating never happening.

   Evacuating now happens if the previous collection increased the
   allocation threshold.

2. The fragmentation status of a block was never cleared after a
   collection. This could result in an increase of fragmentation if the
   collector statistics deemed evacuating to not be needed.

3. The remembered set using a HashSet meant that updating forward
   pointers in the remembered set would result in duplicate entries, as
   the hash codes would be different.

4. Objects promoted to the mature generation would not be remembered if
   they contained any young pointers not promoted. We now trace mature
   objects to check if they need to be remembered, instead of always
   remembering them; drastically reducing the size of the remembered
   set.

Improvements
------------

1. The index to the eden space has been changed to a u8, allowing us to
   add some additional fields without growing the size of a Bucket.

2. We no longer update line maps of mature blocks when performing a
   young collection. This is not needed in a young collection.

3. We now use a chunked list for the remembered set and an additional
   object bit to prevent duplicate entries. This makes the remembered
   set more memory efficient, and removes the need for hashing. This
   does require the use of a 64-bits architecture, but Inko never really
   officially supported 32-bits anyway.

4. Objects with a single hole are not taken into account when
   calculating fragmentation statuses. This way we don't overestimate
   the number of free lines as much, reducing the amount of evacuating
   objects.

5. Messages are now copied into a process' local heap directly, instead
   of being copied into a separate mailbox heap. This reduces memory
   needed when sending messages, overall memory usage, and quite
   drastically simplifies the mailbox and garbage collection
   implementation. Access to the mailbox is synchronised using a
   spinlock, which is fast enough to be used by a mutator.

6. Various garbage collection timers (e.g. the time spent preparing a
   collection) that weren't very useful have been removed.

7. Parallel tracing performance has been improved by tracing objects in
   parallel, instead of only processing stack frames in parallel. When
   moving of objects is not needed this can significantly reduce garbage
   collection timings.

8. The INKO_CONCURRENCY variable has been replaced with separate
   variables, as greater control is needed over some of the thread pool
   settings.

9. GC timings can now be printed when setting INKO_PRINT_GC_TIMINGS to
   "true". This is an internal option and will only stick around until
   we have a better way of aggregating and presenting VM/GC statistics.

10. Updating of garbage collection statistics at the end of a collection
    has been improved a bit, removing the need for counting the total
    number of blocks separately. The performance improvement of this is
    minor, but it does clean up the code quite a bit.
  • Loading branch information
yorickpeterse committed Nov 15, 2019
1 parent cb17da3 commit 347ade2
Show file tree
Hide file tree
Showing 55 changed files with 2,980 additions and 3,923 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -30,7 +30,7 @@ website](https://inko-lang.org/about/).
Inko officially supports Linux, Mac OS, and Windows (when compiled with a MingW
toolchain such as [MSYS2](http://www.msys2.org/)). Other Unix-like platforms
such as the various BSDs should also work, but are not officially supported at
this time.
this time. Inko only supports 64-bits architectures.

## Requirements

Expand Down
55 changes: 7 additions & 48 deletions vm/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vm/Cargo.toml
Expand Up @@ -3,6 +3,7 @@ name = "inko"
version = "0.5.0" # VERSION
authors = ["Yorick Peterse <yorickpeterse@gmail.com>"]
edition = "2018"
build = "build.rs"

[features]
default = []
Expand All @@ -19,7 +20,6 @@ test = false
[dependencies]
getopts = "^0.2"
num_cpus = "^1.10"
rayon = "^1.0"
parking_lot = "^0.7"
fnv = "^1.0"
time = "^0.1"
Expand Down
5 changes: 5 additions & 0 deletions vm/build.rs
@@ -0,0 +1,5 @@
fn main() {
if !cfg!(target_arch = "x86_64") {
panic!("The Inko virtual machine requires a 64-bits architecture");
}
}
85 changes: 19 additions & 66 deletions vm/src/binding.rs
Expand Up @@ -4,9 +4,8 @@
use crate::arc_without_weak::ArcWithoutWeak;
use crate::block::Block;
use crate::chunk::Chunk;
use crate::gc::work_list::WorkList;
use crate::immix::copy_object::CopyObject;
use crate::object_pointer::ObjectPointer;
use crate::object_pointer::{ObjectPointer, ObjectPointerPointer};
use std::cell::UnsafeCell;

pub struct Binding {
Expand Down Expand Up @@ -92,18 +91,20 @@ impl Binding {
unsafe { &mut *self.locals.get() }
}

/// Pushes all pointers in this binding into the supplied vector.
pub fn push_pointers(&self, pointers: &mut WorkList) {
pub fn each_pointer<F>(&self, mut callback: F)
where
F: FnMut(ObjectPointerPointer),
{
let mut current = Some(self);

while let Some(binding) = current {
pointers.push(binding.receiver.pointer());
callback(binding.receiver.pointer());

for index in 0..binding.locals().len() {
let local = &binding.locals()[index];

if !local.is_null() {
pointers.push(local.pointer());
callback(local.pointer());
}
}

Expand Down Expand Up @@ -139,34 +140,12 @@ impl Binding {
parent,
})
}

// Moves all pointers in this binding to the given heap.
pub fn move_pointers_to<H: CopyObject>(&mut self, heap: &mut H) {
if let Some(ref mut bind) = self.parent {
bind.move_pointers_to(heap);
}

{
let locals = self.locals_mut();

for index in 0..locals.len() {
let pointer = locals[index];

if !pointer.is_null() {
locals[index] = heap.move_object(pointer);
}
}
}

self.receiver = heap.move_object(self.receiver);
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::gc::work_list::WorkList;
use crate::immix::global_allocator::GlobalAllocator;
use crate::immix::local_allocator::LocalAllocator;
use crate::object_pointer::ObjectPointer;
Expand Down Expand Up @@ -295,7 +274,7 @@ mod tests {
}

#[test]
fn test_push_pointers() {
fn test_each_pointer() {
let mut alloc =
LocalAllocator::new(GlobalAllocator::with_rc(), &Config::new());

Expand All @@ -311,27 +290,29 @@ mod tests {
binding2.parent = Some(binding1.clone());
binding2.set_local(0, local2);

let mut pointers = WorkList::new();
let mut pointer_pointers = Vec::new();

binding2.push_pointers(&mut pointers);
binding2.each_pointer(|ptr| pointer_pointers.push(ptr));

assert!(*pointers.pop().unwrap().get() == receiver);
assert!(*pointers.pop().unwrap().get() == local2);
let pointers: Vec<_> =
pointer_pointers.into_iter().map(|x| *x.get()).collect();

assert!(*pointers.pop().unwrap().get() == receiver);
assert!(*pointers.pop().unwrap().get() == local1);
assert_eq!(pointers.iter().filter(|x| **x == receiver).count(), 2);
assert!(pointers.contains(&local2));
assert!(pointers.contains(&local1));
}

#[test]
fn test_push_pointers_and_update() {
fn test_each_pointer_and_update() {
let mut alloc =
LocalAllocator::new(GlobalAllocator::with_rc(), &Config::new());

let mut binding = Binding::with_rc(1, alloc.allocate_empty());
let mut pointers = WorkList::new();
let mut pointers = Vec::new();

binding.set_local(0, alloc.allocate_empty());
binding.push_pointers(&mut pointers);

binding.each_pointer(|ptr| pointers.push(ptr));

while let Some(pointer_pointer) = pointers.pop() {
let pointer = pointer_pointer.get_mut();
Expand Down Expand Up @@ -377,34 +358,6 @@ mod tests {
assert_eq!(bind_copy_parent.receiver.float_value().unwrap(), 8.0);
}

#[test]
fn test_move_pointers_to() {
let galloc = GlobalAllocator::with_rc();
let mut alloc1 = LocalAllocator::new(galloc.clone(), &Config::new());
let mut alloc2 = LocalAllocator::new(galloc, &Config::new());

let ptr1 = alloc1.allocate_without_prototype(object_value::float(5.0));
let ptr2 = alloc1.allocate_without_prototype(object_value::float(2.0));
let ptr3 = alloc1.allocate_without_prototype(object_value::float(8.0));

let mut src_bind1 = Binding::with_rc(1, ptr3);
let mut src_bind2 = Binding::with_rc(1, ptr3);

src_bind2.parent = Some(src_bind1.clone());
src_bind1.set_local(0, ptr1);
src_bind2.set_local(0, ptr2);
src_bind2.move_pointers_to(&mut alloc2);

// The original pointers now point to empty objects.
assert!(ptr1.get().value.is_none());
assert!(ptr2.get().value.is_none());
assert!(ptr3.get().value.is_none());

assert_eq!(src_bind2.get_local(0).float_value().unwrap(), 2.0);
assert_eq!(src_bind1.get_local(0).float_value().unwrap(), 5.0);
assert_eq!(src_bind1.receiver.float_value().unwrap(), 8.0);
}

#[test]
fn test_receiver_with_receiver() {
let pointer = ObjectPointer::integer(5);
Expand Down

0 comments on commit 347ade2

Please sign in to comment.