Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
krl committed Jul 2, 2019
0 parents commit 2623f9a
Show file tree
Hide file tree
Showing 6 changed files with 935 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock
13 changes: 13 additions & 0 deletions Cargo.toml
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"
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions README.md
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)
}
```

1 change: 1 addition & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
max_width = 80
205 changes: 205 additions & 0 deletions src/lib.rs
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);
}
}

0 comments on commit 2623f9a

Please sign in to comment.