Skip to content
This repository was archived by the owner on Jun 10, 2024. It is now read-only.

Commit f6946fe

Browse files
committed
Numerous fixes and improvements to alloc/term implementation
- Better layout of term flags/values on 32-bit and 64-bit archs - Make CloneIntoProcess generic over a process allocator for easier testing and better reuse of helper functions - Fixes for a variety of small bugs found while testing GC implementation - Better Debug impl for Term and various term types that dumps an easy to read view of a given term. This is also used to provide debug impls of the different process heaps so one can view all terms found on a heap and the addresses at which they are found
1 parent c74126e commit f6946fe

28 files changed

+3279
-956
lines changed

liblumen_alloc/src/borrow/clone_to_process.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::erts::{ProcessControlBlock, Term};
1+
use crate::erts::{AllocInProcess, Term};
22

33
/// This trait represents cloning, like `Clone`, but specifically
44
/// in the context of terms which need to be cloned into the heap
@@ -14,8 +14,8 @@ use crate::erts::{ProcessControlBlock, Term};
1414
/// NOTE: You can implement both `CloneInProcess` and `Clone` for a type,
1515
/// just be aware that any uses of `Clone` will allocate on the global heap
1616
pub trait CloneToProcess {
17-
/// Returns a copy of this value, performing any heap allocations
17+
/// Returns boxed copy of this value, performing any heap allocations
1818
/// using the process heap of `process`, or using heap fragments if
1919
/// there is not enough space for the cloned value
20-
fn clone_to_process(&self, process: &mut ProcessControlBlock) -> Term;
20+
fn clone_to_process<A: AllocInProcess>(&self, process: &mut A) -> Term;
2121
}

liblumen_alloc/src/erts/fragment.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ impl RawFragment {
3838

3939
/// Returns true if the given pointer is contained within this fragment
4040
#[inline]
41-
pub fn contains(&self, ptr: *const Term) -> bool {
41+
pub fn contains<T>(&self, ptr: *const T) -> bool {
4242
let ptr = ptr as usize;
4343
let start = self.data as usize;
4444
let end = unsafe { self.data.offset(self.size as isize) } as usize;
@@ -68,7 +68,7 @@ impl HeapFragment {
6868

6969
/// Returns true if the given pointer is contained within this fragment
7070
#[inline]
71-
pub fn contains(&self, ptr: *const Term) -> bool {
71+
pub fn contains<T>(&self, ptr: *const T) -> bool {
7272
self.raw.contains(ptr)
7373
}
7474

liblumen_alloc/src/erts/process.rs

Lines changed: 121 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ mod flags;
22
pub use self::flags::*;
33

44
mod alloc;
5+
pub use self::alloc::{AllocInProcess, StackPrimitives};
6+
57
mod gc;
68

79
use core::alloc::{AllocErr, Layout};
810
use core::mem;
9-
use core::ptr::NonNull;
11+
use core::ptr::{self, NonNull};
1012
use core::sync::atomic::{AtomicUsize, Ordering};
1113

1214
use intrusive_collections::{LinkedList, UnsafeRef};
13-
1415
use hashbrown::HashMap;
16+
use liblumen_core::locks::SpinLock;
17+
1518

1619
use self::gc::*;
1720
use super::*;
@@ -38,10 +41,8 @@ pub struct ProcessControlBlock {
3841
young: YoungHeap,
3942
// old generation heap
4043
old: OldHeap,
41-
// virtual binary heap
42-
vheap: VirtualBinaryHeap,
4344
// off-heap allocations
44-
off_heap: LinkedList<HeapFragmentAdapter>,
45+
off_heap: SpinLock<LinkedList<HeapFragmentAdapter>>,
4546
off_heap_size: AtomicUsize,
4647
// process dictionary
4748
dictionary: HashMap<Term, Term>,
@@ -60,8 +61,7 @@ impl ProcessControlBlock {
6061
pub fn new(heap: *mut Term, heap_size: usize) -> Self {
6162
let young = YoungHeap::new(heap, heap_size);
6263
let old = OldHeap::default();
63-
let vheap = VirtualBinaryHeap::new(heap_size);
64-
let off_heap = LinkedList::new(HeapFragmentAdapter::new());
64+
let off_heap = SpinLock::new(LinkedList::new(HeapFragmentAdapter::new()));
6565
let dictionary = HashMap::new();
6666
Self {
6767
flags: AtomicProcessFlag::new(ProcessFlag::Default),
@@ -73,7 +73,6 @@ impl ProcessControlBlock {
7373
max_gen_gcs: 65535,
7474
young,
7575
old,
76-
vheap,
7776
off_heap,
7877
off_heap_size: AtomicUsize::new(0),
7978
dictionary,
@@ -92,26 +91,6 @@ impl ProcessControlBlock {
9291
self.flags.clear(flags);
9392
}
9493

95-
/// Perform a heap allocation.
96-
///
97-
/// If space on the process heap is not immediately available, then the allocation
98-
/// will be pushed into a heap fragment which will then be later moved on to the
99-
/// process heap during garbage collection
100-
#[inline]
101-
pub unsafe fn alloc(&mut self, need: usize) -> Result<NonNull<Term>, AllocErr> {
102-
match self.young.alloc(need) {
103-
ok @ Ok(_) => ok,
104-
Err(_) => self.alloc_fragment(need),
105-
}
106-
}
107-
108-
/// Same as `alloc`, but takes a `Layout` rather than the size in words
109-
#[inline]
110-
pub unsafe fn alloc_layout(&mut self, layout: Layout) -> Result<NonNull<Term>, AllocErr> {
111-
let words = Self::layout_to_words(layout);
112-
self.alloc(words)
113-
}
114-
11594
/// Perform a heap allocation, but do not fall back to allocating a heap fragment
11695
/// if the process heap is not able to fulfill the allocation request
11796
#[inline]
@@ -149,101 +128,47 @@ impl ProcessControlBlock {
149128
let frag_ref = frag.as_ref();
150129
let size = frag_ref.size();
151130
let data = frag_ref.data().cast::<Term>();
152-
self.off_heap.push_front(UnsafeRef::from_raw(frag.as_ptr()));
131+
let mut off_heap = self.off_heap.lock();
132+
off_heap.push_front(UnsafeRef::from_raw(frag.as_ptr()));
133+
drop(off_heap);
153134
self.off_heap_size.fetch_add(size, Ordering::AcqRel);
154135
Ok(data)
155136
}
156137

157-
fn layout_to_words(layout: Layout) -> usize {
158-
let size = layout.size();
159-
let mut words = size / mem::size_of::<Term>();
160-
if size % mem::size_of::<Term>() != 0 {
161-
words += 1;
162-
}
163-
words
164-
}
165-
166-
/// Perform a stack allocation
167-
#[inline]
168-
pub unsafe fn alloca(&mut self, need: usize) -> Result<NonNull<Term>, AllocErr> {
169-
self.young.stack_alloc(need)
170-
}
171-
172-
/// Perform a stack allocation, but with a `Layout`
173-
#[inline]
174-
pub unsafe fn alloca_layout(&mut self, layout: Layout) -> Result<NonNull<Term>, AllocErr> {
175-
let need = to_word_size(layout.size());
176-
self.young.stack_alloc(need)
177-
}
178-
179138
/// Frees stack space occupied by the last term on the stack,
180139
/// adjusting the stack pointer accordingly.
181140
///
182141
/// Use `stack_popn` to pop multiple terms from the stack at once
183142
#[inline]
184-
pub unsafe fn stack_pop(&mut self) {
185-
self.stack_popn(1)
143+
pub fn stack_pop(&mut self) -> Option<Term> {
144+
match self.stack_top() {
145+
None => None,
146+
ok @ Some(_) => {
147+
self.stack_popn(1);
148+
ok
149+
}
150+
}
186151
}
187152

188-
/// Like `stack_pop`, but frees `n` terms from the stack
153+
/// Pushes an immediate term or reference to term/list on top of the stack
154+
///
155+
/// Returns `Err(AllocErr)` if the process is out of stack space
189156
#[inline]
190-
pub unsafe fn stack_popn(&mut self, n: usize) {
191-
assert!(n > 0);
192-
self.young.stack_popn(n);
157+
pub fn stack_push(&mut self, term: Term) -> Result<(), AllocErr> {
158+
assert!(term.is_immediate() || term.is_boxed() || term.is_list());
159+
unsafe {
160+
let stack0 = self.alloca(1)?.as_ptr();
161+
ptr::write(stack0, term);
162+
}
163+
Ok(())
193164
}
194165

195166
/// Returns the term at the top of the stack
196167
#[inline]
197-
pub unsafe fn stack_top(&mut self) -> Option<Term> {
168+
pub fn stack_top(&mut self) -> Option<Term> {
198169
self.stack_slot(1)
199170
}
200171

201-
/// Returns the term located in the given stack slot
202-
///
203-
/// The stack slot index counts upwards from 1, so 1
204-
/// is equivalent to calling `stack_top`, 2 is the
205-
/// term immediately preceding `stack_top` in the stack,
206-
/// and so on
207-
#[inline]
208-
pub unsafe fn stack_slot(&mut self, slot: usize) -> Option<Term> {
209-
assert!(slot > 0);
210-
self.young.stack_slot(slot)
211-
}
212-
213-
/// Returns the number of terms allocated on the stack
214-
#[inline]
215-
pub fn stack_size(&mut self) -> usize {
216-
self.young.stack_size()
217-
}
218-
219-
/// Returns the size of the stack space availabe in units of `Term`
220-
#[inline]
221-
pub fn stack_available(&mut self) -> usize {
222-
self.young.stack_available()
223-
}
224-
225-
/// Pushes a reference-counted binary on to this processes virtual heap
226-
///
227-
/// NOTE: It is expected that the binary reference (the actual `ProcBin` struct)
228-
/// has already been allocated on the process heap, and that this function is
229-
/// being called simply to add the reference to the virtual heap
230-
#[inline]
231-
pub fn vheap_push(&mut self, bin: &ProcBin) -> Term {
232-
self.vheap.push(bin)
233-
}
234-
235-
/// Returns a boolean for if the given pointer is owned by memory allocated to this process
236-
#[inline]
237-
pub fn is_owner(&mut self, ptr: *const Term) -> bool {
238-
if self.young.contains(ptr) || self.old.contains(ptr) {
239-
return true;
240-
}
241-
if self.off_heap.iter().any(|frag| frag.contains(ptr)) {
242-
return true;
243-
}
244-
self.vheap.contains(ptr)
245-
}
246-
247172
/// Puts a new value under the given key in the process dictionary
248173
#[inline]
249174
pub fn put(&mut self, key: Term, value: Term) -> Term {
@@ -326,10 +251,15 @@ impl ProcessControlBlock {
326251
return true;
327252
}
328253
// Check if virtual heap size indicates we should do a collection
329-
let used = self.vheap.heap_used();
330-
let unused = self.vheap.unused();
331-
let threshold = ((used + unused) as f64 * self.gc_threshold).ceil() as usize;
332-
used >= threshold
254+
let used = self.young.virtual_heap_used();
255+
let unused = self.young.virtual_heap_unused();
256+
if unused > 0 {
257+
let threshold = ((used + unused) as f64 * self.gc_threshold).ceil() as usize;
258+
used >= threshold
259+
} else {
260+
// We've exceeded the virtual heap size
261+
true
262+
}
333263
}
334264

335265
#[inline(always)]
@@ -371,7 +301,7 @@ impl ProcessControlBlock {
371301
// we are able to pick up from the current process context
372302
let mut rootset = RootSet::new(roots);
373303
// The primary source of roots we add is the process stack
374-
rootset.push_range(self.young.stack_start, self.young.stack_used());
304+
rootset.push_range(self.young.stack_pointer(), self.young.stack_size());
375305
// The process dictionary is also used for roots
376306
for (k, v) in &self.dictionary {
377307
rootset.push(k as *const _ as *mut _);
@@ -393,17 +323,97 @@ impl Drop for ProcessControlBlock {
393323
// the dictionary is dropped. This leaves only the young and old heap
394324

395325
// Free young heap
396-
let young_heap_start = self.young.start;
326+
let young_heap_start = self.young.heap_start();
397327
let young_heap_size = self.young.size();
398328
unsafe { alloc::free(young_heap_start, young_heap_size) };
399329
// Free old heap, if active
400330
if self.old.active() {
401-
let old_heap_start = self.old.start;
331+
let old_heap_start = self.old.heap_start();
402332
let old_heap_size = self.old.size();
403333
unsafe { alloc::free(old_heap_start, old_heap_size) };
404334
}
405335
}
406336
}
337+
impl AllocInProcess for ProcessControlBlock {
338+
#[inline]
339+
unsafe fn alloc(&mut self, need: usize) -> Result<NonNull<Term>, AllocErr> {
340+
match self.young.alloc(need) {
341+
ok @ Ok(_) => ok,
342+
Err(_) => self.alloc_fragment(need),
343+
}
344+
}
345+
346+
#[inline]
347+
unsafe fn alloca(&mut self, need: usize) -> Result<NonNull<Term>, AllocErr> {
348+
self.young.alloca(need)
349+
}
350+
351+
#[inline]
352+
unsafe fn alloca_unchecked(&mut self, need: usize) -> NonNull<Term> {
353+
self.young.alloca_unchecked(need)
354+
}
355+
356+
#[inline]
357+
fn virtual_alloc(&mut self, bin: &ProcBin) -> Term {
358+
self.young.virtual_alloc(bin)
359+
}
360+
361+
#[inline]
362+
fn is_owner<T>(&mut self, ptr: *const T) -> bool {
363+
if self.young.contains(ptr) || self.old.contains(ptr) {
364+
return true;
365+
}
366+
if self.young.virtual_heap_contains(ptr) || self.old.virtual_heap_contains(ptr) {
367+
return true;
368+
}
369+
let off_heap = self.off_heap.lock();
370+
if off_heap.iter().any(|frag| frag.contains(ptr)) {
371+
return true;
372+
}
373+
false
374+
}
375+
}
376+
impl StackPrimitives for ProcessControlBlock {
377+
#[inline]
378+
fn stack_size(&self) -> usize {
379+
self.young.stack_size()
380+
}
381+
382+
#[inline]
383+
unsafe fn set_stack_size(&mut self, size: usize) {
384+
self.young.set_stack_size(size);
385+
}
386+
387+
#[inline]
388+
fn stack_pointer(&mut self) -> *mut Term {
389+
self.young.stack_pointer()
390+
}
391+
392+
#[inline]
393+
unsafe fn set_stack_pointer(&mut self, sp: *mut Term) {
394+
self.young.set_stack_pointer(sp);
395+
}
396+
397+
#[inline]
398+
fn stack_used(&self) -> usize {
399+
self.young.stack_used()
400+
}
401+
402+
#[inline]
403+
fn stack_available(&self) -> usize {
404+
self.young.stack_available()
405+
}
406+
407+
#[inline]
408+
fn stack_slot(&mut self, n: usize) -> Option<Term> {
409+
self.young.stack_slot(n)
410+
}
411+
412+
#[inline]
413+
fn stack_popn(&mut self, n: usize) {
414+
self.young.stack_popn(n);
415+
}
416+
}
407417

408418
#[cfg(test)]
409419
mod test;

0 commit comments

Comments
 (0)