Skip to content

Commit

Permalink
Clippy, docs, and CI
Browse files Browse the repository at this point in the history
Oh my
  • Loading branch information
ecton committed Mar 12, 2024
1 parent 2c39892 commit 7f5a0f9
Show file tree
Hide file tree
Showing 11 changed files with 435 additions and 155 deletions.
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: [ecton]
28 changes: 28 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Docs

on: [push, pull_request]

jobs:
docs:
runs-on: ubuntu-latest
steps:
- name: Install Rust
uses: hecrj/setup-rust-action@v1
with:
rust-version: nightly

- uses: actions/checkout@v3
- name: Generate Docs
run: |
cargo +nightly doc --no-deps --all-features
- name: Deploy Docs
if: github.ref == 'refs/heads/main'
uses: JamesIves/github-pages-deploy-action@releases/v4
with:
branch: gh-pages
folder: target/doc/
git-config-name: kl-botsu
git-config-email: [email protected]
target-folder: /main/
clean: true
51 changes: 51 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Tests

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3

- name: Install Rust
uses: hecrj/setup-rust-action@v1

- name: Run clippy
run: |
cargo clippy
- name: Run unit tests
run: |
cargo test --all-targets
miri:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3

- name: Install Rust
uses: hecrj/setup-rust-action@v1
with:
components: cargo,rustc,rust-std,miri
rust-version: nightly

- name: Run unit tests
run: |
cargo +nightly miri test
env:
MIRIFLAGS: "-Zmiri-permissive-provenance -Zmiri-ignore-leaks"

build-msrv:
name: Test on MSRV
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: hecrj/setup-rust-action@v1
with:
rust-version: 1.65
- name: Run unit tests
run: cargo test --all-targets
7 changes: 7 additions & 0 deletions Cargo.lock

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

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
name = "musegc"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
rust-version = "1.73.0"

[dependencies]
ahash = "0.8.11"
crossbeam-utils = "0.8.19"
flume = "0.11.0"
intentional = "0.1.1"
kempt = "0.2.3"
nanorand = { version = "0.7.0", default-features = false, features = [
"std",
Expand All @@ -20,6 +20,9 @@ parking_lot = { version = "0.12.1", features = ["arc_lock"] }
criterion = "0.5"
timings = { path = "../timings" }

[lints.clippy]
pedantic = "warn"

[[bench]]
name = "vs-arc"
harness = false
Expand Down
72 changes: 52 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,38 @@
An easy-to-use, incremental, multi-threaded garbage collector for Rust.

```rust
use musegc::{CollectionGuard, Root, Ref};

// Execute a closure with access to a garbage collector.
musegc::collected(|| {
let mut guard = CollectionGuard::acquire();
// Allocate a vec![Ref(1), Ref(2), Ref(3)].
let values: Vec<Ref<u32>> = (1..=3).map(|value| Ref::new(value, &mut guard)).collect();
let values = Root::new(values, &mut guard);
drop(guard);

// Manually execute the garbage collector. Our data will not be freed,
// since `values` is a "root" reference.
musegc::collect();

// Root references allow direct access to their data, even when a
// `CollectionGuard` isn't held.
let (one, two, three) = (values[0], values[1], values[2]);

// Accessing the data contained in a `Ref` requires a guard, however.
let mut guard = CollectionGuard::acquire();
assert_eq!(one.load(&guard), Some(&1));
assert_eq!(two.load(&guard), Some(&2));
assert_eq!(three.load(&guard), Some(&3));

// Dropping our root will allow the collector to free our `Ref`s.
drop(values);
guard.collect();
assert!(one.load(&guard).is_none());
});
```

## Motivation

While working on [Muse][muse], @Ecton recognized [the need for garbage
Expand All @@ -22,20 +54,20 @@ Tracing garbage collectors can be implemented in various ways to identify the
"roots" of known memory so that they can trace from the roots through all
necessary references to determine which memory allocations can be freed.

This crate exposes a `Strong<T>` type which behaves similarly to an `Arc<T>` but
automatically becomes a root for the collector. `Strong<T>` implements
This crate exposes a `Root<T>` type which behaves similarly to an `Arc<T>` but
automatically becomes a root for the collector. `Root<T>` implements
`Deref<Target = T>`, allowing access to the underlying data even while the
collector is running.

The `Weak<T>` type implements `Copy` and does not provide direct access to the
The `Ref<T>` type implements `Copy` and does not provide direct access to the
underlying data. To get a reference to the underlying data, a weak reference
must be upgraded using a `CollectionGuard`. The returned reference is tied to
the guard, which prevents collection from running while any guards are held.

A `CollectionGuard` is needed to:

- Allocate a new `Strong<T>` or `Weak<T>`
- Upgrade a `Weak<T>` to an `&T`
- Allocate a new `Root<T>` or `Ref<T>`
- Load an `&T` from a `Ref<T>`

## Safety

Expand All @@ -61,9 +93,9 @@ triggered by incorrectly using the API or implementing the `Collectable` trait
incorrectly. **Incorrect usage of this crate can lead to deadlocks and memory
leaks.** Specifically:

- Reference cycles between `Strong<T>`'s will lead to leaks just as `Arc<T>`'s
- Reference cycles between `Root<T>`'s will lead to leaks just as `Arc<T>`'s
will.
- If a `Strong<T>` uses locking for interior mutability, holding a lock without
- If a `Root<T>` uses locking for interior mutability, holding a lock without
a collector guard can cause the garbage collector to block until the lock is
released. This escalates from a pause to a deadlock if the lock can't be
released without acquiring a collection guard. **All locks should acquired and
Expand All @@ -83,57 +115,57 @@ leaks.** Specifically:

Benchmarking is hard. These benchmarks aren't adequate. These numbers are from
executing `benches/timings.rs`, which compares allocating 100,000 32-byte values,
comparing the time it takes to allocate each `Arc<[u8; 32]>`, `Strong<[u8;32]>`,
and `Weak<[u8; 32]>`. The measurements are the amount of time it takes for an
comparing the time it takes to allocate each `Arc<[u8; 32]>`, `Root<[u8;32]>`,
and `Ref<[u8; 32]>`. The measurements are the amount of time it takes for an
individual allocation. These results are from running on a Ryzen 3700X.

### 1 thread

| Label | avg | min | max | stddev | out% |
|--------|---------|---------|---------|---------|--------|
| Arc | 47.87ns | 20.00ns | 11.27us | 151.4ns | 0.010% |
| Strong | 62.02ns | 30.00ns | 182.2us | 1.367us | 0.000% |
| Weak | 65.89ns | 30.00ns | 439.2us | 1.556us | 0.001% |
| Root | 62.02ns | 30.00ns | 182.2us | 1.367us | 0.000% |
| Ref | 65.89ns | 30.00ns | 439.2us | 1.556us | 0.001% |

### 4 threads

| Label | avg | min | max | stddev | out% |
|--------|---------|---------|---------|---------|--------|
| Arc | 47.21ns | 20.00ns | 5.810us | 138.0ns | 0.010% |
| Strong | 147.5ns | 30.00ns | 314.4us | 3.849us | 0.001% |
| Weak | 71.03ns | 30.00ns | 633.3us | 2.464us | 0.000% |
| Root | 147.5ns | 30.00ns | 314.4us | 3.849us | 0.001% |
| Ref | 71.03ns | 30.00ns | 633.3us | 2.464us | 0.000% |

### 8 threads

| Label | avg | min | max | stddev | out% |
|--------|---------|---------|---------|---------|--------|
| Arc | 48.91ns | 20.00ns | 65.56us | 172.4ns | 0.010% |
| Strong | 186.8ns | 30.00ns | 729.9us | 6.224us | 0.000% |
| Weak | 109.4ns | 30.00ns | 1.136ms | 5.817us | 0.000% |
| Root | 186.8ns | 30.00ns | 729.9us | 6.224us | 0.000% |
| Ref | 109.4ns | 30.00ns | 1.136ms | 5.817us | 0.000% |

### 16 threads

| Label | avg | min | max | stddev | out% |
|--------|---------|---------|---------|---------|--------|
| Arc | 55.47ns | 20.00ns | 248.9us | 354.6ns | 0.010% |
| Strong | 323.1ns | 30.00ns | 3.105ms | 18.29us | 0.000% |
| Weak | 206.6ns | 30.00ns | 3.492ms | 16.00us | 0.000% |
| Root | 323.1ns | 30.00ns | 3.105ms | 18.29us | 0.000% |
| Ref | 206.6ns | 30.00ns | 3.492ms | 16.00us | 0.000% |

### 32 threads

| Label | avg | min | max | stddev | out% |
|--------|---------|---------|---------|---------|--------|
| Arc | 67.12ns | 20.00ns | 260.0us | 783.3ns | 0.001% |
| Strong | 616.7ns | 30.00ns | 11.91ms | 55.58us | 0.000% |
| Weak | 432.5ns | 30.00ns | 13.66ms | 49.14us | 0.000% |
| Root | 616.7ns | 30.00ns | 11.91ms | 55.58us | 0.000% |
| Ref | 432.5ns | 30.00ns | 13.66ms | 49.14us | 0.000% |

### Author's Benchmark Summary

In these benchmarks, 100 allocations are collected into a pre-allocated `Vec`.
The `Vec` is cleared, and then the process is repeated 1,000 total times
yielding 100,000 total allocations.

In both the `Strong` and `Weak` benchmarks, explicit calls to
In both the `Root` and `Ref` benchmarks, explicit calls to
`CollectorGuard::yield_to_collector()` are placed after the `Vec` is cleared.
The measurements include time waiting for the incremental garbage collector to
run during these yield points.
Expand Down
24 changes: 13 additions & 11 deletions benches/timings.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
//! A multi-threaded benchmark
use std::convert::Infallible;
use std::hint::black_box;
use std::sync::Arc;

use musegc::{collected, CollectionGuard, Strong, Weak};
use musegc::{collected, CollectionGuard, Ref, Root};
use timings::{Benchmark, BenchmarkImplementation, Label, LabeledTimings, Timings};

const TOTAL_ITERS: usize = 100_000;
Expand All @@ -15,8 +17,8 @@ fn main() {
Benchmark::default()
.with_each_number_of_threads([1, 4, 8, 16, 32])
.with::<StdArc>()
.with::<GcWeak>()
.with::<GcStrong>()
.with::<GcRef>()
.with::<GcRoot>()
.run(&timings)
.unwrap();

Expand Down Expand Up @@ -71,11 +73,11 @@ impl BenchmarkImplementation<Label, (), Infallible> for StdArc {
}
}

struct GcWeak {
struct GcRef {
metric: Label,
}

impl BenchmarkImplementation<Label, (), Infallible> for GcWeak {
impl BenchmarkImplementation<Label, (), Infallible> for GcRef {
type SharedConfig = Label;

fn label(_number_of_threads: usize, _config: &()) -> Label {
Expand All @@ -102,13 +104,13 @@ impl BenchmarkImplementation<Label, (), Infallible> for GcWeak {
}

fn measure(&mut self, measurements: &LabeledTimings<Label>) -> Result<(), Infallible> {
let mut allocated = Vec::<Weak<[u8; 32]>>::with_capacity(ITERS_PER_RELEASE);
let mut allocated = Vec::<Ref<[u8; 32]>>::with_capacity(ITERS_PER_RELEASE);
collected(|| {
let mut guard = CollectionGuard::acquire();
for _ in 0..OUTER_ITERS {
for i in 0..ITERS_PER_RELEASE {
let timing = measurements.begin(self.metric.clone());
let result = black_box(Weak::new([0; 32], &mut guard));
let result = black_box(Ref::new([0; 32], &mut guard));
if i == ITERS_PER_RELEASE - 1 {
allocated.clear();
guard.yield_to_collector();
Expand All @@ -123,11 +125,11 @@ impl BenchmarkImplementation<Label, (), Infallible> for GcWeak {
}
}

struct GcStrong {
struct GcRoot {
metric: Label,
}

impl BenchmarkImplementation<Label, (), Infallible> for GcStrong {
impl BenchmarkImplementation<Label, (), Infallible> for GcRoot {
type SharedConfig = Label;

fn label(_number_of_threads: usize, _config: &()) -> Label {
Expand All @@ -154,13 +156,13 @@ impl BenchmarkImplementation<Label, (), Infallible> for GcStrong {
}

fn measure(&mut self, measurements: &LabeledTimings<Label>) -> Result<(), Infallible> {
let mut allocated = Vec::<Strong<[u8; 32]>>::with_capacity(ITERS_PER_RELEASE);
let mut allocated = Vec::<Root<[u8; 32]>>::with_capacity(ITERS_PER_RELEASE);
collected(|| {
let mut guard = CollectionGuard::acquire();
for _ in 0..OUTER_ITERS {
for i in 0..ITERS_PER_RELEASE {
let timing = measurements.begin(self.metric.clone());
let result = black_box(Strong::new([0; 32], &mut guard));
let result = black_box(Root::new([0; 32], &mut guard));
if i == ITERS_PER_RELEASE - 1 {
allocated.clear();
guard.yield_to_collector();
Expand Down
Loading

0 comments on commit 7f5a0f9

Please sign in to comment.