Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
82 changes: 43 additions & 39 deletions crates/pvm-bump-allocator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,20 @@
#![no_std]

use core::alloc::{GlobalAlloc, Layout};
use core::sync::atomic::{AtomicUsize, Ordering};
use core::cell::{Cell, UnsafeCell};

/// A bump allocator backed by a fixed-size heap.
///
/// `HEAP_SIZE` is the total number of bytes available for allocation.
/// Memory is never freed — `dealloc` is a no-op.
pub struct BumpAllocator<const HEAP_SIZE: usize> {
offset: AtomicUsize,
heap: core::cell::UnsafeCell<[u8; HEAP_SIZE]>,
offset: Cell<usize>,
heap: UnsafeCell<[u8; HEAP_SIZE]>,
}

// SAFETY: The allocator uses atomic operations for the offset, so it is safe to share
// across threads (though PVM contracts are single-threaded, this satisfies the
// `GlobalAlloc` requirement).
// SAFETY: PVM contracts are single-threaded. The `Sync` bound is required by
// `GlobalAlloc` (the allocator must live in a `static`), but no concurrent
// access actually occurs.
unsafe impl<const HEAP_SIZE: usize> Sync for BumpAllocator<HEAP_SIZE> {}

impl<const HEAP_SIZE: usize> Default for BumpAllocator<HEAP_SIZE> {
Expand All @@ -43,8 +43,8 @@ impl<const HEAP_SIZE: usize> BumpAllocator<HEAP_SIZE> {
/// Creates a new bump allocator with a zeroed heap of `HEAP_SIZE` bytes.
pub const fn new() -> Self {
Self {
offset: AtomicUsize::new(0),
heap: core::cell::UnsafeCell::new([0u8; HEAP_SIZE]),
offset: Cell::new(0),
heap: UnsafeCell::new([0u8; HEAP_SIZE]),
}
}
}
Expand All @@ -54,31 +54,25 @@ unsafe impl<const HEAP_SIZE: usize> GlobalAlloc for BumpAllocator<HEAP_SIZE> {
let align = layout.align();
let size = layout.size();

let mut current = self.offset.load(Ordering::Relaxed);

loop {
let aligned = (current + align - 1) & !(align - 1);
let Some(next) = aligned.checked_add(size) else {
return core::ptr::null_mut();
};

if next > HEAP_SIZE {
return core::ptr::null_mut();
}

match self.offset.compare_exchange_weak(
current,
next,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => {
let heap_ptr = self.heap.get() as *mut u8;
return unsafe { heap_ptr.add(aligned) };
}
Err(observed) => current = observed,
}
let current = self.offset.get();
let aligned = (current + align - 1) & !(align - 1);
let Some(next) = aligned.checked_add(size) else {
core::panic!("exhausted heap limit");
};

if next > HEAP_SIZE {
core::panic!("exhausted heap limit");
}

self.offset.set(next);
let heap_ptr = self.heap.get() as *mut u8;
unsafe { heap_ptr.add(aligned) }
}

// The heap is zero-initialized and memory is never reused, so every
// region returned by `alloc` is already zeroed.
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
unsafe { self.alloc(layout) }
}

unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {}
Expand Down Expand Up @@ -117,25 +111,35 @@ mod tests {
let alloc = BumpAllocator::<64>::new();
let layout = Layout::from_size_align(64, 1).unwrap();
assert!(!unsafe { alloc.alloc(layout) }.is_null());
}

// Heap is full — next alloc must fail
assert!(unsafe { alloc.alloc(Layout::from_size_align(1, 1).unwrap()) }.is_null());
#[test]
#[should_panic = "exhausted heap limit"]
fn alloc_panics_when_full() {
let alloc = BumpAllocator::<64>::new();
unsafe {
alloc.alloc(Layout::from_size_align(64, 1).unwrap());
alloc.alloc(Layout::from_size_align(1, 1).unwrap());
}
}

#[test]
fn alloc_oom_returns_null() {
#[should_panic = "exhausted heap limit"]
fn alloc_oom_panics() {
let alloc = BumpAllocator::<16>::new();
let layout = Layout::from_size_align(17, 1).unwrap();
assert!(unsafe { alloc.alloc(layout) }.is_null());
unsafe { alloc.alloc(Layout::from_size_align(17, 1).unwrap()) };
}

#[test]
#[should_panic = "exhausted heap limit"]
fn alloc_oom_due_to_alignment_padding() {
// 9 bytes of heap: alloc 1 byte, then try 8 bytes with align 8
// offset=1, aligned=8, 8+8=16 > 9 → OOM
let alloc = BumpAllocator::<9>::new();
unsafe { alloc.alloc(Layout::from_size_align(1, 1).unwrap()) };
assert!(unsafe { alloc.alloc(Layout::from_size_align(8, 8).unwrap()) }.is_null());
unsafe {
alloc.alloc(Layout::from_size_align(1, 1).unwrap());
alloc.alloc(Layout::from_size_align(8, 8).unwrap());
}
}

#[test]
Expand Down
36 changes: 18 additions & 18 deletions crates/pvm-contract-benchmarks/reports/encoding-benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,27 @@ used in this report.

### Static Types (no-alloc compatible)

| Type | Operation | pvm-contract-types | alloy-core | Ratio |
|----------|-----------|--------------------|--------------|--------------|
| u8 | encode | 14.31 ns | 6.49 ns | 2.20x slower |
| u8 | decode | 0.40 ns | 2.37 ns | 5.97x faster |
| u32 | encode | 14.23 ns | 6.86 ns | 2.08x slower |
| u32 | decode | 0.98 ns | 2.36 ns | 2.41x faster |
| u128 | encode | 14.07 ns | 6.67 ns | 2.11x slower |
| u128 | decode | 0.99 ns | 2.79 ns | 2.83x faster |
| U256 | encode | 13.74 ns | 6.51 ns | 2.11x slower |
| U256 | decode | 1.78 ns | 3.57 ns | 2.01x faster |
| address | encode | 15.50 ns | 11.52 ns | 1.35x slower |
| address | decode | 7.09 ns | 9.59 ns | 1.35x faster |
| Type | Operation | pvm-contract-types | alloy-core | Ratio |
|------|-----------|--------------------|------------|-------|
| u8 | encode | 13.26 ns | 6.38 ns | 2.08x slower |
| u8 | decode | 0.39 ns | 2.35 ns | 6.07x faster |
| u32 | encode | 13.51 ns | 6.85 ns | 1.97x slower |
| u32 | decode | 0.97 ns | 2.40 ns | 2.46x faster |
| u128 | encode | 14.19 ns | 6.63 ns | 2.14x slower |
| u128 | decode | 0.98 ns | 2.77 ns | 2.82x faster |
| U256 | encode | 13.40 ns | 6.40 ns | 2.09x slower |
| U256 | decode | 1.76 ns | 3.53 ns | 2.01x faster |
| address | encode | 14.78 ns | 11.24 ns | 1.31x slower |
| address | decode | 6.91 ns | 9.45 ns | 1.37x faster |

### Dynamic Types (alloc required)

| Type | Operation | pvm-contract-types | alloy-core | Ratio |
|------------|-----------|--------------------|--------------|--------------|
| String | encode | 11.24 ns | 16.23 ns | 1.44x faster |
| String | decode | 11.15 ns | 32.70 ns | 2.93x faster |
| Vec\<U256> | encode | 20.78 ns | 25.62 ns | 1.23x faster |
| Vec\<U256> | decode | 25.01 ns | 46.63 ns | 1.87x faster |
| Type | Operation | pvm-contract-types | alloy-core | Ratio |
|------|-----------|--------------------|------------|-------|
| String | encode | 11.04 ns | 15.85 ns | 1.44x faster |
| String | decode | 10.94 ns | 32.57 ns | 2.98x faster |
| Vec\<U256> | encode | 20.42 ns | 25.02 ns | 1.23x faster |
| Vec\<U256> | decode | 23.86 ns | 45.87 ns | 1.92x faster |

### Summary

Expand Down
Loading