-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 2623f9a
Showing
6 changed files
with
935 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/target | ||
**/*.rs.bk | ||
Cargo.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
[package] | ||
authors = ["Kristoffer Ström <kristoffer@rymdkoloni.se>"] | ||
edition = "2018" | ||
name = "bunch" | ||
version = "0.1.0" | ||
description = "Append-only, concurrent arena" | ||
keywords = ["arena", "datastructure"] | ||
documentation = "https://docs.rs/bunch/" | ||
license = "GPL-3.0" | ||
|
||
[dependencies] | ||
arrayvec = "0.4" | ||
parking_lot = "0.4" |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# Bunch | ||
|
||
An append-only, concurrent arena. | ||
|
||
## Guarantees | ||
|
||
Elements pushed into the arena are never moved or modified. All the elements are dropped at once, as the arena is dropped. | ||
|
||
Since the elements does not move in memory, they can be safely concurrently accessed by multiple threads. | ||
|
||
Locking is only neccesary when pushing to the structure, as to guarantee that the allocation logic in different threads are not stepping on each others toes. | ||
|
||
## Drawbacks | ||
|
||
The elements are allocated in multiple slices on the heap, so the range prefix is not supported. Iterators could be supported though. | ||
|
||
## Memory layout | ||
|
||
The slices are arranged in the following pattern in memory: | ||
|
||
``` | ||
[T, T] | ||
[T, T, T, T] | ||
[T, T, T, T, T, T, T, T] | ||
``` | ||
|
||
Each new allocation is double the size of the previous one. | ||
|
||
In order to efficiently calculate the row and column from an index, we can use a special instruction `usize::leading_zeros()`, which acts kind of as a log2 operation. | ||
|
||
```rust | ||
fn lane_offset(offset: usize) -> (usize, usize) { | ||
let i = offset / 2 + 1; | ||
let lane = USIZE_BITS - i.leading_zeros() as usize - 1; | ||
let offset = offset - (2usize.pow(lane as u32) - 1) * 2; | ||
(lane, offset) | ||
} | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
max_width = 80 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
#![deny(missing_docs)] | ||
//! An append-only, concurrent arena | ||
|
||
use std::cell::UnsafeCell; | ||
use std::mem; | ||
|
||
use arrayvec::ArrayVec; | ||
use parking_lot::Mutex; | ||
|
||
const N_LANES: usize = 64; | ||
const USIZE_BITS: usize = mem::size_of::<usize>() * 8; | ||
|
||
#[inline(always)] | ||
fn lane_size(n: usize) -> usize { | ||
2_usize.pow(n as u32) * 2 | ||
} | ||
|
||
#[inline(always)] | ||
fn lane_offset(offset: usize) -> (usize, usize) { | ||
let i = offset / 2 + 1; | ||
let lane = USIZE_BITS - i.leading_zeros() as usize - 1; | ||
let offset = offset - (2usize.pow(lane as u32) - 1) * 2; | ||
(lane, offset) | ||
} | ||
|
||
impl<T> Default for Bunch<T> { | ||
fn default() -> Self { | ||
Bunch { | ||
lanes: Default::default(), | ||
len: Default::default(), | ||
} | ||
} | ||
} | ||
|
||
unsafe impl<T> Send for Bunch<T> {} | ||
unsafe impl<T> Sync for Bunch<T> {} | ||
|
||
/// The main Arena type | ||
pub struct Bunch<T> { | ||
lanes: UnsafeCell<ArrayVec<[Vec<T>; N_LANES]>>, | ||
len: Mutex<usize>, | ||
} | ||
|
||
impl<T> Bunch<T> { | ||
/// Creates a new arena | ||
pub fn new() -> Self { | ||
Self::default() | ||
} | ||
|
||
/// Pushes to the arena, returning a reference to the pushed element | ||
pub fn push(&self, t: T) -> &T { | ||
let len = &mut *self.len.lock(); | ||
let (lane, offset) = lane_offset(*len); | ||
|
||
if offset == 0 { | ||
unsafe { | ||
let lanes = &mut *self.lanes.get(); | ||
let size = lane_size(lane); | ||
lanes.push(Vec::with_capacity(size)); | ||
} | ||
} | ||
|
||
unsafe { | ||
let lanes = &mut *self.lanes.get(); | ||
lanes[lane].push(t); | ||
*len += 1; | ||
lanes[lane].get(offset).expect("just pushed") | ||
} | ||
} | ||
|
||
/// Gets a reference into the arena | ||
pub fn get(&self, idx: usize) -> &T { | ||
let (lane, offset) = lane_offset(idx); | ||
unsafe { &(*self.lanes.get())[lane][offset] } | ||
} | ||
|
||
/// Returns the number of elements in the Bunch | ||
pub fn len(&self) -> usize { | ||
*self.len.lock() | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
|
||
use super::*; | ||
use std::sync::Arc; | ||
|
||
#[test] | ||
fn it_works() { | ||
let p = Bunch::new(); | ||
|
||
assert_eq!(p.push(3), &3); | ||
|
||
assert_eq!(p.get(0), &3); | ||
} | ||
|
||
#[test] | ||
fn multiple() { | ||
let p = Bunch::new(); | ||
|
||
for i in 0..10_000 { | ||
let r = p.push(i); | ||
assert_eq!(r, &i); | ||
} | ||
|
||
for i in 0..10_000 { | ||
assert_eq!(p.get(i), &i); | ||
} | ||
} | ||
|
||
#[test] | ||
fn multithreading() { | ||
let vec = Arc::new(Bunch::new()); | ||
let n = 100_000; | ||
|
||
let n_threads = 16; | ||
|
||
let mut handles = vec![]; | ||
|
||
for t in 0..n_threads { | ||
let vec = vec.clone(); | ||
handles.push(std::thread::spawn(move || { | ||
for i in 0..n { | ||
if i % n_threads == t { | ||
vec.push(i); | ||
} | ||
} | ||
})) | ||
} | ||
|
||
for h in handles { | ||
h.join().unwrap(); | ||
} | ||
|
||
let mut result = vec![]; | ||
|
||
for i in 0..n { | ||
result.push(vec.get(i)); | ||
} | ||
|
||
result.sort(); | ||
|
||
for i in 0..n { | ||
assert_eq!(result[i], &i) | ||
} | ||
} | ||
|
||
#[test] | ||
fn dropping() { | ||
let n = 100_000; | ||
let n_threads = 16; | ||
|
||
let mut arcs = vec![]; | ||
|
||
for i in 0..n { | ||
arcs.push(Arc::new(i)); | ||
} | ||
|
||
for i in 0..n { | ||
assert_eq!(Arc::strong_count(&arcs[i]), 1); | ||
} | ||
|
||
let wrapped_arcs = Arc::new(arcs); | ||
|
||
{ | ||
let vec = Arc::new(Bunch::new()); | ||
|
||
let mut handles = vec![]; | ||
|
||
for t in 0..n_threads { | ||
let vec = vec.clone(); | ||
let wrapped_clone = wrapped_arcs.clone(); | ||
handles.push(std::thread::spawn(move || { | ||
for i in 0..n { | ||
if i % n_threads == t { | ||
vec.push(wrapped_clone[i].clone()); | ||
} | ||
} | ||
})) | ||
} | ||
|
||
for h in handles { | ||
h.join().unwrap(); | ||
} | ||
|
||
for i in 0..n { | ||
assert_eq!(Arc::strong_count(&wrapped_arcs[i]), 2); | ||
} | ||
} | ||
// Bunch dropped here | ||
for i in 0..n { | ||
assert_eq!(Arc::strong_count(&wrapped_arcs[i]), 1); | ||
} | ||
} | ||
|
||
#[test] | ||
#[should_panic] | ||
fn out_of_bounds_access() { | ||
let bunch = Bunch::new(); | ||
bunch.push("hello"); | ||
|
||
bunch.get(1); | ||
} | ||
} |