diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..9a5f865 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,25 @@ +codecov: + require_ci_to_pass: false + +ignore: + - benchmark/ + - src/abort.rs + +coverage: + status: + project: # Overall project status + default: + target: auto + if_not_found: success + only_pulls: false + patch: # Status for the patch in pull requests + default: + target: auto + if_not_found: success + only_pulls: true + changes: false # Whether to comment on the coverage changes in pull requests + +comment: + layout: "header, diff, files, footer" + behavior: default + require_changes: false diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..fd90028 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,64 @@ +name: Benchmark + +on: + push: + branches: + - main + paths-ignore: + - 'README' + - 'COPYRIGHT' + - 'LICENSE-*' + - '**.md' + - '**.txt' + pull_request: + paths-ignore: + - 'README' + - 'COPYRIGHT' + - 'LICENSE-*' + - '**.md' + - '**.txt' + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: -Dwarnings + RUST_BACKTRACE: 1 + nightly: nightly + stable: stable + +jobs: + benchmark: + name: benchmark + strategy: + matrix: + os: + - ubuntu-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - name: Cache cargo build and registry + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-test-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-test- + - name: Install Rust + # --no-self-update is necessary because the windows environment cannot self-update rustup.exe. + run: rustup update stable --no-self-update && rustup default stable + - name: Cache ~/.cargo + uses: actions/cache@v3 + with: + path: ~/.cargo + key: ${{ runner.os }}-coverage-dotcargo + - name: Run test + run: | + cd benchmark + cargo bench + - name: Upload benchmark results + uses: actions/upload-artifact@v4 + with: + name: benchmark-results + path: benchmark/target/criterion diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6ba154..95aad26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ on: - '**.md' - '**.txt' + workflow_dispatch: + env: CARGO_TERM_COLOR: always RUSTFLAGS: -Dwarnings @@ -63,7 +65,7 @@ jobs: - name: Install cargo-hack run: cargo install cargo-hack - name: Apply clippy lints - run: cargo hack clippy --each-feature + run: cargo hack clippy --each-feature --exclude-no-default-features # Run tests on some extra platforms cross: @@ -104,13 +106,13 @@ jobs: run: | cargo install cross cross build --target ${{ matrix.target }} - if: matrix.target != 'wasm32-unknown-unknown' - # # WASM support - # - name: cargo build --target ${{ matrix.target }} - # run: | - # rustup target add ${{ matrix.target }} - # cargo build --features js --target ${{ matrix.target }} - # if: matrix.target == 'wasm32-unknown-unknown' + if: matrix.target != 'wasm32-unknown-unknown' && matrix.target != 'wasm32-wasi' + # WASM support + - name: cargo build --target ${{ matrix.target }} + run: | + rustup target add ${{ matrix.target }} + cargo build --target ${{ matrix.target }} + if: matrix.target == 'wasm32-unknown-unknown' || matrix.target == 'wasm32-wasi' # - name: cargo build --target ${{ matrix.target }} # run: | # rustup target add ${{ matrix.target }} @@ -149,7 +151,7 @@ jobs: path: ~/.cargo key: ${{ runner.os }}-coverage-dotcargo - name: Run build - run: cargo hack build --feature-powerset + run: cargo hack build --feature-powerset --exclude-no-default-features test: name: test @@ -183,7 +185,7 @@ jobs: path: ~/.cargo key: ${{ runner.os }}-coverage-dotcargo - name: Run test - run: cargo hack test --feature-powerset + run: cargo hack test --tests --feature-powerset --exclude-no-default-features --exclude-all-features sanitizer: name: sanitizer @@ -191,8 +193,6 @@ jobs: matrix: os: - ubuntu-latest - - macos-latest - - windows-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 @@ -210,8 +210,6 @@ jobs: run: rustup update $nightly && rustup default $nightly - name: Install rust-src run: rustup component add rust-src - - name: Install cargo-hack - run: cargo install cargo-hack - name: ASAN / LSAN / TSAN run: ci/sanitizer.sh @@ -236,8 +234,6 @@ jobs: key: ${{ runner.os }}-miri-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-miri- - - name: Install cargo-hack - run: cargo install cargo-hack - name: Miri run: ci/miri.sh loom: @@ -263,71 +259,8 @@ jobs: ${{ runner.os }}-loom- - name: Install Rust run: rustup update $nightly && rustup default $nightly - - name: Install cargo-hack - run: cargo install cargo-hack - name: Loom tests - run: RUSTFLAGS="--cfg loom -Dwarnings" cargo hack test --test loom - - # valgrind - valgrind: - name: valgrind - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Cache cargo build and registry - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ubuntu-latest-valgrind-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ubuntu-latest-valgrind- - - - name: Install Rust ${{ env.stable }} - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ env.stable }} - override: true - - name: Install Valgrind - run: | - sudo apt-get update -y - sudo apt-get install -y valgrind - # Compile tests - # - name: cargo build foo - # run: cargo build --bin foo - # working-directory: integration - - # Run with valgrind - # - name: Run valgrind foo - # run: valgrind --error-exitcode=1 --leak-check=full --show-leak-kinds=all ./target/debug/foo - # working-directory: integration - - docs: - name: docs - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Cache cargo build and registry - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ubuntu-latest-docs-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ubuntu-latest-docs- - - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ env.nightly }} - override: true - - name: "doc --lib --all-features" - run: cargo doc --lib --no-deps --all-features - env: - RUSTFLAGS: --cfg docsrs - RUSTDOCFLAGS: --cfg docsrs -Dwarnings + run: cargo test --tests --features loom coverage: name: coverage @@ -339,9 +272,7 @@ jobs: - cross - test - sanitizer - - miri - loom - - docs steps: - uses: actions/checkout@v3 - name: Install latest nightly @@ -367,9 +298,9 @@ jobs: uses: actions-rs/cargo@v1 with: command: tarpaulin - args: --all-features --run-types tests --run-types doctests --workspace --out xml + args: --run-types tests --run-types doctests --workspace --out xml - name: Upload to codecov.io - uses: codecov/codecov-action@v3.1.1 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/.github/workflows/loc.yml b/.github/workflows/loc.yml new file mode 100644 index 0000000..941eb62 --- /dev/null +++ b/.github/workflows/loc.yml @@ -0,0 +1,58 @@ +name: loc + +on: + push: + branches: + - main + paths-ignore: + - 'README.md' + - 'COPYRIGHT' + - 'LICENSE*' + - '**.md' + - '**.txt' + - 'art' + pull_request: + paths-ignore: + - 'README.md' + - 'COPYRIGHT' + - 'LICENSE*' + - '**.md' + - '**.txt' + - 'art' + workflow_dispatch: + +jobs: + loc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install Rust + run: | + rustup update stable && rustup default stable + rustup component add clippy + rustup component add rustfmt + + - name: Install tokeit + run: | + cargo install tokeit --force + + - name: Count total lines of code + run: | + tokeit + - name: Upload total loc to GitHub Gist + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GIST_PAT }} + script: | + const fs = require('fs'); + const output = fs.readFileSync('tokeit.json', 'utf8'); + const gistId = '327b2a8aef9003246e45c6e47fe63937'; + await github.rest.gists.update({ + gist_id: gistId, + files: { + "objectpool": { + content: output + } + } + }); diff --git a/CHANGELOG.md b/CHANGELOG.md index bd7a668..b025a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,6 @@ -# UNRELEASED -# 0.1.2 (January 6th, 2022) - -FEATURES +# CHANGELOG +## 0.1.0 +## UNRELEASED diff --git a/Cargo.toml b/Cargo.toml index 583f5a6..701de24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,37 +1,26 @@ [package] -name = "template-rs" -version = "0.1.6" +name = "objectpool" +version = "0.1.0" edition = "2021" -repository = "https://github.com/al8n/template-rs" -homepage = "https://github.com/al8n/template-rs" -documentation = "https://docs.rs/template-rs" -description = "A template for creating Rust open-source repo on GitHub" -license = "MIT/Apache-2.0" -rust-version = "1.73" - -[[bench]] -path = "benches/foo.rs" -name = "foo" -harness = false +repository = "https://github.com/al8n/objectpool" +homepage = "https://github.com/al8n/objectpool" +documentation = "https://docs.rs/objectpool" +description = "Yet another lock-free object pool, support no_std" +license = "MIT OR Apache-2.0" +rust-version = "1.56" +keywords = ["atomic", "object-pool", "lockfree-object-pool", "non-blocking", "lock-free"] +categories = ["concurrency", "memory-management", "data-structures", "development-tools", "no-std"] +exclude = ["benchmark"] [features] -default = [] +default = ["std"] +alloc = ["crossbeam-queue/alloc"] +std = ["crossbeam-queue/default"] [dependencies] +crossbeam-queue = { version = "0.3", default-features = false } -[dev-dependencies] -criterion = "0.5" -tempfile = "3" - -[profile.bench] -opt-level = 3 -debug = false -codegen-units = 1 -lto = 'thin' -incremental = false -debug-assertions = false -overflow-checks = false -rpath = false +loom = { version = "0.7", optional = true } [package.metadata.docs.rs] all-features = true diff --git a/README-zh_CN.md b/README-zh_CN.md index 21d7466..bfdf383 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -1,50 +1,91 @@
-

template-rs

+

Object Pool

-开源Rust代码库GitHub模版 +Yet another lock-free object pool, support `no_std`. -[github][Github-url] -[Build][CI-url] -[codecov][codecov-url] +[github][Github-url] +LoC +[Build][CI-url] +[codecov][codecov-url] -[docs.rs][doc-url] -[crates.io][crates-url] -[rustc][rustc-url] +[docs.rs][doc-url] +[crates.io][crates-url] +[crates.io][crates-url] +license -[license-apache][license-apache-url] -[license-mit][license-mit-url] - -[English][en-url] | 简体中文 +English | [简体中文][zh-cn-url]
+## Features + +- Lock free, backed by concurrent safe queue. +- Builtin reference counter, no `Arc` wrapper required. +- Simple APIs and support `no_std` environment. +- Automatically put object back to pool. + ## Installation -```toml -[dependencies] -template_rs = "0.1" + +- `std` + + ```toml + [dependencies] + objectpool = "0.1" + ``` + +- `no_std` + + ```toml + [dependencies] + objectpool = { version = "0.1", default-features = false, features = ["alloc"] } + ``` + +## Tests + +- `test`: + + ```sh + cargo test + ``` + +- `miri`: + + ```sh + cargo miri test + ``` + +- `loom`: + + ```sh + cargo test --tests --features loom + ``` + +## Benchmarks + +For detailed reports, you can see the latest Benchmark GitHub Action, and download the Artifacts. + +```sh +cargo bench ``` -## Features -- [x] 更快的创建GitHub开源Rust代码库 +## Why this crate? + +I need an object pool that supports the `no_std` environment. Unfortunately, none of the [`object-pool`](https://crates.io/crates/object-pool), [`lockfree-object-pool`](https://crates.io/crates/lockfree-object-pool) and [`sharded-slab`](https://crates.io/crates/sharded-slab) support `no_std`. #### License -`Template-rs` is under the terms of both the MIT license and the +`objectpool` is under the terms of both the MIT license and the Apache License (Version 2.0). See [LICENSE-APACHE](LICENSE-APACHE), [LICENSE-MIT](LICENSE-MIT) for details. -Copyright (c) 2021 Al Liu. - -[Github-url]: https://github.com/al8n/template-rs/ -[CI-url]: https://github.com/al8n/template/actions/workflows/template.yml -[doc-url]: https://docs.rs/template-rs -[crates-url]: https://crates.io/crates/template-rs -[codecov-url]: https://app.codecov.io/gh/al8n/template-rs/ -[license-url]: https://opensource.org/licenses/Apache-2.0 -[rustc-url]: https://github.com/rust-lang/rust/blob/master/RELEASES.md -[license-apache-url]: https://opensource.org/licenses/Apache-2.0 -[license-mit-url]: https://opensource.org/licenses/MIT -[en-url]: https://github.com/al8n/template-rs/tree/main/README.md +Copyright (c) 2024 Al Liu. + +[Github-url]: https://github.com/al8n/objectpool/ +[CI-url]: https://github.com/al8n/objectpool/actions/workflows/ci.yml +[doc-url]: https://docs.rs/objectpool +[crates-url]: https://crates.io/crates/objectpool +[codecov-url]: https://app.codecov.io/gh/al8n/objectpool/ +[zh-cn-url]: https://github.com/al8n/objectpool/tree/main/README-zh_CN.md diff --git a/README.md b/README.md index 218fb4f..bfdf383 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,91 @@
-

template-rs

+

Object Pool

-A template for creating Rust open-source GitHub repo. +Yet another lock-free object pool, support `no_std`. -[github][Github-url] -[Build][CI-url] -[codecov][codecov-url] - -[docs.rs][doc-url] -[crates.io][crates-url] -[crates.io][crates-url] +[github][Github-url] +LoC +[Build][CI-url] +[codecov][codecov-url] +[docs.rs][doc-url] +[crates.io][crates-url] +[crates.io][crates-url] license English | [简体中文][zh-cn-url]
+## Features + +- Lock free, backed by concurrent safe queue. +- Builtin reference counter, no `Arc` wrapper required. +- Simple APIs and support `no_std` environment. +- Automatically put object back to pool. + ## Installation -```toml -[dependencies] -template_rs = "0.1" + +- `std` + + ```toml + [dependencies] + objectpool = "0.1" + ``` + +- `no_std` + + ```toml + [dependencies] + objectpool = { version = "0.1", default-features = false, features = ["alloc"] } + ``` + +## Tests + +- `test`: + + ```sh + cargo test + ``` + +- `miri`: + + ```sh + cargo miri test + ``` + +- `loom`: + + ```sh + cargo test --tests --features loom + ``` + +## Benchmarks + +For detailed reports, you can see the latest Benchmark GitHub Action, and download the Artifacts. + +```sh +cargo bench ``` -## Features -- [x] Create a Rust open-source repo fast +## Why this crate? + +I need an object pool that supports the `no_std` environment. Unfortunately, none of the [`object-pool`](https://crates.io/crates/object-pool), [`lockfree-object-pool`](https://crates.io/crates/lockfree-object-pool) and [`sharded-slab`](https://crates.io/crates/sharded-slab) support `no_std`. #### License -`Template-rs` is under the terms of both the MIT license and the +`objectpool` is under the terms of both the MIT license and the Apache License (Version 2.0). See [LICENSE-APACHE](LICENSE-APACHE), [LICENSE-MIT](LICENSE-MIT) for details. -Copyright (c) 2021 Al Liu. +Copyright (c) 2024 Al Liu. -[Github-url]: https://github.com/al8n/template-rs/ -[CI-url]: https://github.com/al8n/template-rs/actions/workflows/ci.yml -[doc-url]: https://docs.rs/template-rs -[crates-url]: https://crates.io/crates/template-rs -[codecov-url]: https://app.codecov.io/gh/al8n/template-rs/ -[zh-cn-url]: https://github.com/al8n/template-rs/tree/main/README-zh_CN.md +[Github-url]: https://github.com/al8n/objectpool/ +[CI-url]: https://github.com/al8n/objectpool/actions/workflows/ci.yml +[doc-url]: https://docs.rs/objectpool +[crates-url]: https://crates.io/crates/objectpool +[codecov-url]: https://app.codecov.io/gh/al8n/objectpool/ +[zh-cn-url]: https://github.com/al8n/objectpool/tree/main/README-zh_CN.md diff --git a/benches/foo.rs b/benches/foo.rs deleted file mode 100644 index f328e4d..0000000 --- a/benches/foo.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/benchmark/Cargo.toml b/benchmark/Cargo.toml new file mode 100644 index 0000000..1b50b91 --- /dev/null +++ b/benchmark/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "bench-tools" +version = "0.1.0" +edition = "2021" +publish = false + +[[bench]] +path = "benches/bench.rs" +name = "bench" +harness = false + +[dev-dependencies] +criterion = "0.5.1" +objectpool = { path = "../" } +sharded-slab = "0.1" +object-pool = "0.5" +criterion-plot = "0.5" +lockfree-object-pool = "0.1" \ No newline at end of file diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..0d16d2b --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,2 @@ + +The benchmark code is copied from https://github.com/EVaillant/lockfree-object-pool. \ No newline at end of file diff --git a/benchmark/benches/bench.rs b/benchmark/benches/bench.rs new file mode 100644 index 0000000..e9f1797 --- /dev/null +++ b/benchmark/benches/bench.rs @@ -0,0 +1,519 @@ +#[macro_use] +extern crate criterion; + +use criterion::Criterion; + + + +#[macro_use] +mod bench_generic; + +struct Vec4096 { + _data: Vec, +} + +impl Default for Vec4096 { + fn default() -> Self { + Self { + _data: Vec::with_capacity(16 * 1024), + } + } +} + +impl sharded_slab::Clear for Vec4096 { + fn clear(&mut self) {} +} + +fn bench_alloc(c: &mut Criterion) { + let mut group = c.benchmark_group("allocation"); + bench_alloc_impl_!( + group, + "crate 'objectpool' bounded", + objectpool::Pool::>::bounded(32, || Vec::with_capacity(4096), |_| {}), + 4 + ); + + bench_alloc_impl_!( + group, + "crate 'objectpool' unbounded", + objectpool::Pool::>::unbounded(|| Vec::with_capacity(4096), |_| {}), + 4 + ); + + bench_alloc_impl_!( + group, + "crate 'lockfree-object-pool' none object poll", + lockfree_object_pool::NoneObjectPool::new(|| Vec::::with_capacity(16 * 1024)), + 1 + ); + bench_alloc_impl_!( + group, + "crate 'lockfree-object-pool' mutex object poll", + lockfree_object_pool::MutexObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + ), + 1 + ); + bench_alloc_impl_!( + group, + "crate 'lockfree-object-pool' spin_lock object poll", + lockfree_object_pool::SpinLockObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + ), + 1 + ); + bench_alloc_impl_!( + group, + "crate 'lockfree-object-pool' linear object poll", + lockfree_object_pool::LinearObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + ), + 1 + ); + bench_alloc_impl_!( + group, + "crate 'object-pool'", + object_pool::Pool::>::new(32, || Vec::with_capacity(4096)), + 2 + ); + bench_alloc_impl_!( + group, + "crate 'sharded-slab'", + sharded_slab::Pool::::new(), + 3 + ); + group.finish(); +} + +fn bench_free(c: &mut Criterion) { + let mut group = c.benchmark_group("free"); + bench_free_impl_!( + group, + "crate 'objectpool' bounded", + objectpool::Pool::bounded(32, || Vec::::with_capacity(16 * 1024), |_| {}), + 4 + ); + + bench_free_impl_!( + group, + "crate 'objectpool' unbounded", + objectpool::Pool::unbounded(|| Vec::::with_capacity(16 * 1024), |_| {}), + 4 + ); + + bench_free_impl_!( + group, + "crate 'lockfree-object-pool' none object poll", + lockfree_object_pool::NoneObjectPool::new(|| Vec::::with_capacity(16 * 1024)), + 1 + ); + bench_free_impl_!( + group, + "crate 'lockfree-object-pool' mutex object poll", + lockfree_object_pool::MutexObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + ), + 1 + ); + bench_free_impl_!( + group, + "crate 'lockfree-object-pool' spin_lock object poll", + lockfree_object_pool::SpinLockObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + ), + 1 + ); + bench_free_impl_!( + group, + "crate 'lockfree-object-pool' linear object poll", + lockfree_object_pool::LinearObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + ), + 1 + ); + bench_free_impl_!( + group, + "crate 'object-pool'", + object_pool::Pool::>::new(32, || Vec::with_capacity(4096)), + 2 + ); + bench_free_impl_!( + group, + "crate 'sharded-slab'", + sharded_slab::Pool::::new(), + 3 + ); + group.finish(); +} + +fn bench_reuse(c: &mut Criterion) { + const VEC_SIZE: usize = 16384; + const BATCH_SIZE: usize = 8192; + + let mut group = c.benchmark_group("reuse"); + + group.bench_function("crate 'objectpool' bounded", |b| { + b.iter_batched( + || { + let pool = + objectpool::Pool::bounded(VEC_SIZE, || Vec::::with_capacity(16 * 1024), |_| {}); + let v: Vec<_> = (0..VEC_SIZE).map(|_| pool.get()).collect(); + drop(v); + (pool, Vec::with_capacity(VEC_SIZE)) + }, + |(pool, mut vec)| { + for index in 0..BATCH_SIZE { + vec.insert(index, criterion::black_box(pool.get())); + } + }, + criterion::BatchSize::SmallInput, + ); + }); + + group.bench_function("crate 'objectpool' unbounded", |b| { + b.iter_batched( + || { + let pool = objectpool::Pool::unbounded(|| Vec::::with_capacity(16 * 1024), |_| {}); + let v: Vec<_> = (0..VEC_SIZE).map(|_| pool.get()).collect(); + drop(v); + (pool, Vec::with_capacity(VEC_SIZE)) + }, + |(pool, mut vec)| { + for index in 0..BATCH_SIZE { + vec.insert(index, criterion::black_box(pool.get())); + } + }, + criterion::BatchSize::SmallInput, + ); + }); + + group.bench_function("none object poll", |b| { + b.iter_batched( + || { + ( + lockfree_object_pool::NoneObjectPool::new(|| Vec::::with_capacity(16 * 1024)), + Vec::with_capacity(VEC_SIZE), + ) + }, + |(pool, mut vec)| { + for index in 0..BATCH_SIZE { + vec.insert(index, criterion::black_box(pool.pull())); + } + }, + criterion::BatchSize::SmallInput, + ); + }); + + group.bench_function("mutex object poll", |b| { + b.iter_batched( + || { + let pool = lockfree_object_pool::MutexObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {}, + ); + let v: Vec<_> = (0..VEC_SIZE).map(|_| pool.pull()).collect(); + drop(v); + (pool, Vec::with_capacity(VEC_SIZE)) + }, + |(pool, mut vec)| { + for index in 0..BATCH_SIZE { + vec.insert(index, criterion::black_box(pool.pull())); + } + }, + criterion::BatchSize::SmallInput, + ); + }); + + group.bench_function("spin_lock object poll", |b| { + b.iter_batched( + || { + let pool = lockfree_object_pool::SpinLockObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {}, + ); + let v: Vec<_> = (0..VEC_SIZE).map(|_| pool.pull()).collect(); + drop(v); + (pool, Vec::with_capacity(VEC_SIZE)) + }, + |(pool, mut vec)| { + for index in 0..BATCH_SIZE { + vec.insert(index, criterion::black_box(pool.pull())); + } + }, + criterion::BatchSize::SmallInput, + ); + }); + + group.bench_function("linear object poll", |b| { + b.iter_batched( + || { + let pool = lockfree_object_pool::LinearObjectPool::new( + || Vec::::with_capacity(16 * 1024), + |_v| {}, + ); + let v: Vec<_> = (0..VEC_SIZE).map(|_| pool.pull()).collect(); + drop(v); + (pool, Vec::with_capacity(VEC_SIZE)) + }, + |(pool, mut vec)| { + for index in 0..BATCH_SIZE { + vec.insert(index, criterion::black_box(pool.pull())); + } + }, + criterion::BatchSize::SmallInput, + ); + }); + + group.bench_function("crate 'object-pool'", |b| { + b.iter_batched( + || { + let pool = object_pool::Pool::new(VEC_SIZE, || Vec::::with_capacity(16 * 1024)); + let v: Vec<_> = (0..VEC_SIZE).map(|_| pool.try_pull().unwrap()).collect(); + drop(v); + (pool, Vec::with_capacity(VEC_SIZE)) + }, + |(pool, mut vec)| { + for index in 0..BATCH_SIZE { + vec.insert(index, criterion::black_box(pool.try_pull().unwrap())); + } + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +fn bench_alloc_mt(c: &mut Criterion) { + let mut group = c.benchmark_group("multi thread allocation"); + bench_alloc_mt_impl_!( + group, + "crate 'objectpool' bounded", + objectpool::Pool::>::bounded(32, || Vec::with_capacity(4096), |_| {}), + 4 + ); + + bench_alloc_mt_impl_!( + group, + "crate 'objectpool' unbounded", + objectpool::Pool::>::unbounded(|| Vec::with_capacity(4096), |_| {}), + 4 + ); + + bench_alloc_mt_impl_!( + group, + "none object poll", + Arc::new(lockfree_object_pool::NoneObjectPool::new(|| Vec::::with_capacity(16 * 1024))), + 1 + ); + bench_alloc_mt_impl_!( + group, + "mutex object poll", + Arc::new(lockfree_object_pool::MutexObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + )), + 1 + ); + bench_alloc_mt_impl_!( + group, + "spin_lock object poll", + Arc::new(lockfree_object_pool::SpinLockObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + )), + 1 + ); + bench_alloc_mt_impl_!( + group, + "linear object poll", + Arc::new(lockfree_object_pool::LinearObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + )), + 1 + ); + bench_alloc_mt_impl_!( + group, + "crate 'object-pool'", + Arc::new(object_pool::Pool::>::new(32, || Vec::with_capacity(4096))), + 2 + ); + bench_alloc_mt_impl_!( + group, + "crate 'sharded-slab'", + Arc::new(sharded_slab::Pool::::new()), + 3 + ); + group.finish(); +} + +fn bench_free_mt(c: &mut Criterion) { + let mut group = c.benchmark_group("multi thread free"); + bench_free_mt_impl_!( + group, + "crate 'objectpool' bounded", + objectpool::Pool::>::bounded(32, || Vec::with_capacity(4096), |_| {}), + 4 + ); + + bench_free_mt_impl_!( + group, + "crate 'objectpool' unbounded", + objectpool::Pool::>::unbounded(|| Vec::with_capacity(4096), |_| {}), + 4 + ); + + bench_free_mt_impl_!( + group, + "none object poll", + Arc::new(lockfree_object_pool::NoneObjectPool::new(|| Vec::::with_capacity(16 * 1024))), + 1 + ); + bench_free_mt_impl_!( + group, + "mutex object poll", + Arc::new(lockfree_object_pool::MutexObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + )), + 1 + ); + bench_free_mt_impl_!( + group, + "spin_lock object poll", + Arc::new(lockfree_object_pool::SpinLockObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + )), + 1 + ); + bench_free_mt_impl_!( + group, + "linear object poll", + Arc::new(lockfree_object_pool::LinearObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + )), + 1 + ); + bench_free_mt_impl_!( + group, + "crate 'object-pool'", + Arc::new(object_pool::Pool::>::new(32, || Vec::with_capacity(4096))), + 2 + ); + bench_free_mt_impl_!( + group, + "crate 'sharded-slab'", + Arc::new(sharded_slab::Pool::::new()), + 3 + ); + group.finish(); +} + +fn bench_forward_multi_thread(c: &mut Criterion, nb_writter: usize, nb_readder: usize) { + let mut group = c.benchmark_group(format!( + "forward msg from pull (nb_writter:{} nb_readder:{})", + nb_writter, nb_readder + )); + bench_forward_impl_!( + group, + "crate 'objectpool' bounded", + objectpool::Pool::>::bounded(32, || Vec::with_capacity(16 * 1024), |_v| {}), + nb_readder, + nb_writter, + 4 + ); + + bench_forward_impl_!( + group, + "crate 'objectpool' unbounded", + objectpool::Pool::>::unbounded(|| Vec::with_capacity(16 * 1024), |_v| {}), + nb_readder, + nb_writter, + 4 + ); + + bench_forward_impl_!( + group, + "none object poll", + Arc::new(lockfree_object_pool::NoneObjectPool::new(|| Vec::::with_capacity(16 * 1024))), + nb_readder, + nb_writter, + 1 + ); + bench_forward_impl_!( + group, + "mutex object poll", + Arc::new(lockfree_object_pool::MutexObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + )), + nb_readder, + nb_writter, + 1 + ); + bench_forward_impl_!( + group, + "spin_lock object poll", + Arc::new(lockfree_object_pool::SpinLockObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + )), + nb_readder, + nb_writter, + 1 + ); + bench_forward_impl_!( + group, + "linear object poll", + Arc::new(lockfree_object_pool::LinearObjectPool::>::new( + || Vec::with_capacity(16 * 1024), + |_v| {} + )), + nb_readder, + nb_writter, + 1 + ); + bench_forward_impl_!( + group, + "crate 'sharded-slab'", + Arc::new(sharded_slab::Pool::::new()), + nb_readder, + nb_writter, + 3 + ); + group.finish(); +} + +fn bench_forward_multi_thread55(c: &mut Criterion) { + bench_forward_multi_thread(c, 5, 5); +} + +fn bench_forward_multi_thread15(c: &mut Criterion) { + bench_forward_multi_thread(c, 1, 5); +} + +fn bench_forward_multi_thread51(c: &mut Criterion) { + bench_forward_multi_thread(c, 5, 1); +} + +fn bench_forward_multi_thread11(c: &mut Criterion) { + bench_forward_multi_thread(c, 1, 1); +} + +criterion_group!( + forward_multi_thread, + bench_forward_multi_thread55, + bench_forward_multi_thread15, + bench_forward_multi_thread51, + bench_forward_multi_thread11 +); +criterion_group!(multi_thread, bench_alloc_mt, bench_free_mt); +criterion_group!(mono_thread, bench_alloc, bench_free, bench_reuse); +criterion_main!(mono_thread, multi_thread, forward_multi_thread); diff --git a/benchmark/benches/bench_generic.rs b/benchmark/benches/bench_generic.rs new file mode 100644 index 0000000..2fa46a3 --- /dev/null +++ b/benchmark/benches/bench_generic.rs @@ -0,0 +1,214 @@ +#[macro_export] +macro_rules! pull_ { + ($pool:ident, 1) => { + $pool.pull() + }; + ($pool:ident, 2) => { + $pool.pull(|| Vec::with_capacity(4096)) + }; + ($pool:ident, 3) => { + $pool.create().unwrap() + }; + ($pool:ident, 4) => { + $pool.get() + }; +} + +#[macro_export] +macro_rules! pull_forward_ { + ($pool:ident, 1) => { + $pool.pull_owned() + }; + ($pool:ident, 3) => { + $pool.clone().create_owned().unwrap() + }; + ($pool:ident, 4) => { + $pool.get_owned() + }; +} + +#[macro_export] +macro_rules! bench_alloc_impl_ { + ($group:expr, $name:literal, $expression:expr, $pull_impl:tt) => { + $group.bench_function($name, |b| { + let pool = $expression; + let mut items = Vec::new(); + b.iter(|| { + items.push(pull_!(pool, $pull_impl)); + }); + }); + }; +} + +#[macro_export] +macro_rules! bench_free_impl_ { + ($group:expr, $name:literal, $expression:expr, $pull_impl:tt) => { + $group.bench_function($name, |b| { + b.iter_custom(|iter| { + use std::time::Instant; + let pool = $expression; + let mut items = Vec::new(); + for _ in 0..iter { + items.push(pull_!(pool, $pull_impl)); + } + let start = Instant::now(); + items.clear(); + start.elapsed() + }); + }); + }; +} + +#[macro_export] +macro_rules! bench_alloc_mt_impl_ { + ($group:expr, $name:literal, $expression:expr, $pull_impl:tt) => { + $group.bench_function($name, |b| { + b.iter_custom(|iter| { + use std::sync::Arc; + use std::sync::Barrier; + use std::thread; + use std::time::Instant; + + let pool = $expression; + let start_barrier = Arc::new(Barrier::new(6)); + let stop_barrier = Arc::new(Barrier::new(6)); + let mut children = Vec::new(); + for _ in 0..5 { + let pool = pool.clone(); + let start_barrier = Arc::clone(&start_barrier); + let stop_barrier = Arc::clone(&stop_barrier); + let child = thread::spawn(move || { + let mut items = Vec::with_capacity(iter as usize); + start_barrier.wait(); + for _ in 0..iter { + items.push(pull_!(pool, $pull_impl)); + } + stop_barrier.wait(); + }); + children.push(child); + } + + start_barrier.wait(); + let start = Instant::now(); + stop_barrier.wait(); + let duration = start.elapsed() / 5; + + for child in children { + child.join().unwrap(); + } + + duration + }); + }); + }; +} + +#[macro_export] +macro_rules! bench_free_mt_impl_ { + ($group:expr, $name:literal, $expression:expr, $pull_impl:tt) => { + $group.bench_function($name, |b| { + b.iter_custom(|iter| { + use std::sync::Arc; + use std::sync::Barrier; + use std::thread; + use std::time::Instant; + let pool = $expression; + let start_barrier = Arc::new(Barrier::new(6)); + let stop_barrier = Arc::new(Barrier::new(6)); + let mut children = Vec::new(); + for _ in 0..5 { + let pool = pool.clone(); + let start_barrier = Arc::clone(&start_barrier); + let stop_barrier = Arc::clone(&stop_barrier); + let child = thread::spawn(move || { + let mut items = Vec::with_capacity(iter as usize); + for _ in 0..iter { + items.push(pull_!(pool, $pull_impl)); + } + start_barrier.wait(); + items.clear(); + stop_barrier.wait(); + }); + children.push(child); + } + + start_barrier.wait(); + let start = Instant::now(); + stop_barrier.wait(); + let duration = start.elapsed() / 5; + + for child in children { + child.join().unwrap(); + } + + duration + }); + }); + }; +} + +#[macro_export] +macro_rules! bench_forward_impl_ { + ($group:expr, $name:literal, $expression:expr, $nb_readder:ident, $nb_writter:ident, $pull_impl:tt) => { + $group.bench_function($name, |b| { + b.iter_custom(|iter| { + use bench_tools::Queue; + use std::sync::Arc; + use std::sync::Barrier; + use std::thread; + use std::time::Instant; + + let pool = $expression; + + let queue = Arc::new(Queue::new()); + let start_barrier = Arc::new(Barrier::new($nb_readder + $nb_writter + 1)); + let stop_reader_barrier = Arc::new(Barrier::new($nb_readder + 1)); + let stop_writer_barrier = Arc::new(Barrier::new($nb_writter + 1)); + let mut children = Vec::new(); + for _ in 0..$nb_readder { + let queue = Arc::clone(&queue); + let start_barrier = Arc::clone(&start_barrier); + let stop_reader_barrier = Arc::clone(&stop_reader_barrier); + let child = thread::spawn(move || { + start_barrier.wait(); + loop { + let elt = queue.pop(); + if elt.is_none() { + break; + } + } + stop_reader_barrier.wait(); + }); + children.push(child); + } + for _ in 0..$nb_writter { + let pool = pool.clone(); + let queue = Arc::clone(&queue); + let start_barrier = Arc::clone(&start_barrier); + let stop_writer_barrier = Arc::clone(&stop_writer_barrier); + let child = thread::spawn(move || { + start_barrier.wait(); + for _ in 0..iter { + queue.push(pull_forward_!(pool, $pull_impl)) + } + stop_writer_barrier.wait(); + }); + children.push(child); + } + + start_barrier.wait(); + let start = Instant::now(); + stop_writer_barrier.wait(); + queue.stop(); + stop_reader_barrier.wait(); + let duration = start.elapsed() / 5; + + for child in children { + child.join().unwrap(); + } + + duration + }); + }); + }; +} diff --git a/benchmark/src/lib.rs b/benchmark/src/lib.rs new file mode 100644 index 0000000..670efa3 --- /dev/null +++ b/benchmark/src/lib.rs @@ -0,0 +1,57 @@ +use std::collections::VecDeque; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Condvar, Mutex}; + +pub struct Queue { + data: Mutex>, + condvar: Condvar, + stop: AtomicBool, +} + +impl Queue { + pub fn new() -> Self { + Self { + data: Mutex::new(VecDeque::new()), + condvar: Condvar::new(), + stop: AtomicBool::new(false), + } + } + + pub fn push(&self, value: T) { + let mut datas = self.data.lock().unwrap(); + let empty = datas.is_empty(); + datas.push_back(value); + if empty { + self.condvar.notify_all(); + } + } + + pub fn stop(&self) { + let _datas = self.data.lock().unwrap(); + self.stop.store(true, Ordering::Relaxed); + self.condvar.notify_all(); + } + + pub fn pop(&self) -> Option { + let mut datas = self.data.lock().unwrap(); + if !datas.is_empty() { + datas.pop_front() + } else if self.stop.load(Ordering::Relaxed) { + None + } else { + self + .condvar + .wait_while(datas, |datas| { + datas.is_empty() && !self.stop.load(Ordering::Relaxed) + }) + .unwrap() + .pop_front() + } + } +} + +impl Default for Queue { + fn default() -> Self { + Self::new() + } +} diff --git a/ci/miri.sh b/ci/miri.sh index 7ea1a2c..35ec5f4 100755 --- a/ci/miri.sh +++ b/ci/miri.sh @@ -7,5 +7,7 @@ cargo miri setup export MIRIFLAGS="-Zmiri-strict-provenance -Zmiri-disable-isolation -Zmiri-symbolic-alignment-check" -cargo hack miri test --each-feature - +cargo miri test --tests --target x86_64-unknown-linux-gnu +cargo miri test --tests --target aarch64-unknown-linux-gnu +cargo miri test --tests --target i686-unknown-linux-gnu +cargo miri test --tests --target powerpc64-unknown-linux-gnu diff --git a/ci/sanitizer.sh b/ci/sanitizer.sh index a21beb3..c8b9eee 100755 --- a/ci/sanitizer.sh +++ b/ci/sanitizer.sh @@ -4,14 +4,14 @@ set -ex export ASAN_OPTIONS="detect_odr_violation=0 detect_leaks=0" -# Run address sanitizer with cargo-hack +# Run address sanitizer RUSTFLAGS="-Z sanitizer=address" \ -cargo hack test --lib --each-feature +cargo test --tests --target x86_64-unknown-linux-gnu -# Run leak sanitizer with cargo-hack +# Run leak sanitizer RUSTFLAGS="-Z sanitizer=leak" \ -cargo hack test --lib --each-feature +cargo test --tests --target x86_64-unknown-linux-gnu -# Run thread sanitizer with cargo-hack +# Run thread sanitizer RUSTFLAGS="-Z sanitizer=thread" \ -cargo hack -Zbuild-std test --lib --each-feature +cargo -Zbuild-std test --tests --target x86_64-unknown-linux-gnu diff --git a/examples/foo.rs b/examples/foo.rs deleted file mode 100644 index f328e4d..0000000 --- a/examples/foo.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/src/abort.rs b/src/abort.rs new file mode 100644 index 0000000..14a6d64 --- /dev/null +++ b/src/abort.rs @@ -0,0 +1,20 @@ +#[inline(never)] +#[cold] +pub(super) fn abort() -> ! { + #[cfg(feature = "std")] + { + std::process::abort() + } + + #[cfg(not(feature = "std"))] + { + struct Abort; + impl Drop for Abort { + fn drop(&mut self) { + panic!(); + } + } + let _a = Abort; + panic!("abort"); + } +} diff --git a/src/lib.rs b/src/lib.rs index dbdf2a0..515d93b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,730 @@ -//! A template for creating Rust open-source repo on GitHub +#![doc = include_str!("../README.md")] +#![cfg_attr(not(any(feature = "std", test)), no_std)] #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, allow(unused_attributes))] #![deny(missing_docs)] -/// template -pub fn it_works() -> usize { - 4 +#[cfg(not(feature = "std"))] +extern crate alloc as std; + +#[cfg(feature = "std")] +extern crate std; + +#[cfg(not(any(feature = "std", feature = "alloc")))] +compile_error!("`objectpool` requires either the 'std' or 'alloc' feature to be enabled."); + +use core::{mem::ManuallyDrop, ptr::NonNull}; + +use crossbeam_queue::{ArrayQueue, SegQueue}; + +#[cfg(not(feature = "loom"))] +use core::sync::atomic::{AtomicPtr, AtomicUsize, Ordering}; + +#[cfg(feature = "loom")] +use loom::sync::atomic::{AtomicPtr, AtomicUsize, Ordering}; + +#[cfg(not(feature = "std"))] +use std::boxed::Box; + +mod abort; + +/// A reusable `T`. +pub struct ReusableObject { + pool: Pool, + obj: ManuallyDrop, +} + +impl AsRef for ReusableObject { + fn as_ref(&self) -> &T { + &self.obj + } +} + +impl AsMut for ReusableObject { + fn as_mut(&mut self) -> &mut T { + &mut self.obj + } +} + +impl core::ops::Deref for ReusableObject { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.obj + } +} + +impl core::ops::DerefMut for ReusableObject { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.obj + } +} + +impl Drop for ReusableObject { + fn drop(&mut self) { + // SAFETY: The object is dropped, we never reuse the ManuallyDrop again. + unsafe { + self.pool.attach(ManuallyDrop::take(&mut self.obj)); + } + } +} + +/// A reusable `T`. +pub struct ReusableObjectRef<'a, T> { + pool: &'a Pool, + obj: ManuallyDrop, +} + +impl<'a, T> AsRef for ReusableObjectRef<'a, T> { + fn as_ref(&self) -> &T { + &self.obj + } +} + +impl<'a, T> AsMut for ReusableObjectRef<'a, T> { + fn as_mut(&mut self) -> &mut T { + &mut self.obj + } +} + +impl<'a, T> core::ops::Deref for ReusableObjectRef<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.obj + } +} + +impl<'a, T> core::ops::DerefMut for ReusableObjectRef<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.obj + } +} + +impl<'a, T> Drop for ReusableObjectRef<'a, T> { + fn drop(&mut self) { + // SAFETY: The object is dropped, we never reuse the ManuallyDrop again. + unsafe { + self.pool.attach(ManuallyDrop::take(&mut self.obj)); + } + } +} + +// It is ok to have a large enum variant here because the enum will always be `Box::into_raw(Box::new(_))` +#[allow(clippy::large_enum_variant)] +enum Backed { + Bounded(ArrayQueue), + Unbounded(SegQueue), +} + +struct Queue { + refs: AtomicUsize, + queue: Backed, +} + +impl Queue { + #[inline] + fn bounded(queue: ArrayQueue) -> Self { + Self { + refs: AtomicUsize::new(1), + queue: Backed::Bounded(queue), + } + } + + #[inline] + fn unbounded(queue: SegQueue) -> Self { + Self { + refs: AtomicUsize::new(1), + queue: Backed::Unbounded(queue), + } + } + + #[inline] + fn push(&self, obj: T) { + match &self.queue { + Backed::Bounded(queue) => { + let _ = queue.push(obj); + } + Backed::Unbounded(queue) => queue.push(obj), + } + } + + #[inline] + fn pop(&self) -> Option { + match &self.queue { + Backed::Bounded(queue) => queue.pop(), + Backed::Unbounded(queue) => queue.pop(), + } + } +} + +/// Lock-free object pool. +pub struct Pool { + refs: AtomicPtr<()>, + queue: *mut Queue, + new: NonNull T + Send + Sync + 'static>, + reset: NonNull, +} + +unsafe impl Send for Pool {} +unsafe impl Sync for Pool {} + +impl Pool { + /// Create a new pool with the given capacity. + /// + /// # Example + /// + /// ```rust + /// use objectpool::Pool; + /// + /// let pool = Pool::::bounded(10, Default::default, |_v| {}); + /// ``` + #[inline] + pub fn bounded( + capacity: usize, + new: impl Fn() -> T + Send + Sync + 'static, + reset: impl Fn(&mut T) + Send + Sync + 'static, + ) -> Self { + let queue = Queue::bounded(ArrayQueue::::new(capacity)); + Self::new(queue, new, reset) + } + + /// Create a new pool with the unbounded capacity. + /// + /// # Example + /// + /// ```rust + /// use objectpool::Pool; + /// + /// let pool = Pool::::unbounded(Default::default, |_v| {}); + /// ``` + #[inline] + pub fn unbounded( + new: impl Fn() -> T + Send + Sync + 'static, + reset: impl Fn(&mut T) + Send + Sync + 'static, + ) -> Self { + let queue = Queue::unbounded(SegQueue::::new()); + Self::new(queue, new, reset) + } + + /// Get an object from the pool. + /// + /// # Example + /// + /// ```rust + /// use objectpool::Pool; + /// + /// let pool = Pool::::bounded(10, Default::default, |_v| {}); + /// + /// let mut obj = pool.get(); + /// + /// assert_eq!(*obj, 0); + /// + /// *obj = 42; + /// drop(obj); + /// ``` + #[inline] + pub fn get(&self) -> ReusableObjectRef { + ReusableObjectRef { + pool: self, + obj: ManuallyDrop::new(self.queue().pop().unwrap_or_else(|| self.new_object())), + } + } + + /// Get an object from the pool. + /// + /// # Example + /// + /// ```rust + /// use objectpool::Pool; + /// + /// let pool = Pool::::bounded(10, Default::default, |_v| {}); + /// + /// let mut obj = pool.get_owned(); + /// + /// assert_eq!(*obj, 0); + /// + /// *obj = 42; + /// drop(obj); + /// ``` + #[inline] + pub fn get_owned(&self) -> ReusableObject { + ReusableObject { + pool: self.clone(), + obj: ManuallyDrop::new(self.queue().pop().unwrap_or_else(|| self.new_object())), + } + } + + /// Get an object from the pool with a fallback. + /// + /// # Example + /// + /// ```rust + /// use objectpool::Pool; + /// + /// let pool = Pool::::bounded(10, Default::default, |_| {}); + /// + /// let mut obj = pool.get_or_else(|| 42); + /// + /// assert_eq!(*obj, 42); + /// ``` + #[inline] + pub fn get_or_else(&self, fallback: impl Fn() -> T) -> ReusableObjectRef { + ReusableObjectRef { + pool: self, + obj: ManuallyDrop::new(self.queue().pop().unwrap_or_else(fallback)), + } + } + + /// Get an object from the pool with a fallback. + /// + /// # Example + /// + /// ```rust + /// use objectpool::Pool; + /// + /// let pool = Pool::::bounded(10, Default::default, |_| {}); + /// + /// let mut obj = pool.get_owned_or_else(|| 42); + /// + /// assert_eq!(*obj, 42); + /// ``` + #[inline] + pub fn get_owned_or_else(&self, fallback: impl Fn() -> T) -> ReusableObject { + ReusableObject { + pool: self.clone(), + obj: ManuallyDrop::new(self.queue().pop().unwrap_or_else(fallback)), + } + } + + /// Clear the pool. + /// + /// # Example + /// + /// ```rust + /// use objectpool::Pool; + /// + /// let pool = Pool::::bounded(10, Default::default, |v| {}); + /// + /// let mut obj = pool.get(); + /// *obj = 42; + /// drop(obj); + /// + /// pool.clear(); + /// ``` + #[inline] + pub fn clear(&self) { + while self.queue().pop().is_some() {} + } + + #[inline] + fn new( + queue: Queue, + new: impl Fn() -> T + Send + Sync + 'static, + reset: impl Fn(&mut T) + Send + Sync + 'static, + ) -> Self { + let ptr = Box::into_raw(Box::new(queue)); + + unsafe { + Self { + queue: ptr, + refs: AtomicPtr::new(ptr as *mut ()), + // SAFETY: Box::new is safe because the closure is 'static. + new: NonNull::new_unchecked(Box::into_raw(Box::new(new))), + // SAFETY: Box::new is safe because the closure is 'static. + reset: NonNull::new_unchecked(Box::into_raw(Box::new(reset))), + } + } + } + + /// Return an object to the pool. + #[inline] + fn attach(&self, mut obj: T) { + self.reset_object(&mut obj); + self.queue().push(obj); + } + + #[inline] + fn new_object(&self) -> T { + // SAFETY: The new closure is 'static and the pointer is valid until the last Pool instance is droped. + let constructor = unsafe { &*(self.new.as_ptr()) }; + constructor() + } + + #[inline] + fn reset_object(&self, obj: &mut T) { + // SAFETY: The reset closure is 'static and the pointer is valid until the last Pool instance is droped. + let resetter = unsafe { &*(self.reset.as_ptr()) }; + resetter(obj); + } + + #[inline] + fn queue(&self) -> &Queue { + // SAFETY: The pointer is valid until the last Pool instance is droped. + unsafe { &*self.queue } + } +} + +impl Clone for Pool { + fn clone(&self) -> Self { + unsafe { + let shared: *mut Queue = self.refs.load(Ordering::Relaxed).cast(); + + let old_size = (*shared).refs.fetch_add(1, Ordering::Release); + if old_size > usize::MAX >> 1 { + abort::abort(); + } + + // SAFETY: The ptr is always non-null, and the data is only deallocated when the + // last Pool is dropped. + Self { + refs: AtomicPtr::new(shared as *mut ()), + queue: self.queue, + new: self.new, + reset: self.reset, + } + } + } +} + +impl Drop for Pool { + fn drop(&mut self) { + unsafe { + self.refs.with_mut(|shared| { + let shared: *mut Queue = shared.cast(); + // `Shared` storage... follow the drop steps from Arc. + if (*shared).refs.fetch_sub(1, Ordering::Release) != 1 { + return; + } + + // This fence is needed to prevent reordering of use of the data and + // deletion of the data. Because it is marked `Release`, the decreasing + // of the reference count synchronizes with this `Acquire` fence. This + // means that use of the data happens before decreasing the reference + // count, which happens before this fence, which happens before the + // deletion of the data. + // + // As explained in the [Boost documentation][1], + // + // > It is important to enforce any possible access to the object in one + // > thread (through an existing reference) to *happen before* deleting + // > the object in a different thread. This is achieved by a "release" + // > operation after dropping a reference (any access to the object + // > through this reference must obviously happened before), and an + // > "acquire" operation before deleting the object. + // + // [1]: (www.boost.org/doc/libs/1_55_0/doc/html/atomic/usage_examples.html) + // + // Thread sanitizer does not support atomic fences. Use an atomic load + // instead. + (*shared).refs.load(Ordering::Acquire); + + // Drop the data + let _ = Box::from_raw(shared); + let _ = Box::from_raw(self.new.as_ptr()); + let _ = Box::from_raw(self.reset.as_ptr()); + }); + } + } +} + +#[cfg(not(feature = "loom"))] +trait AtomicMut { + fn with_mut(&mut self, f: F) -> R + where + F: FnOnce(&mut *mut T) -> R; +} + +#[cfg(not(feature = "loom"))] +impl AtomicMut for AtomicPtr { + fn with_mut(&mut self, f: F) -> R + where + F: FnOnce(&mut *mut T) -> R, + { + f(self.get_mut()) + } } #[cfg(test)] mod tests { use super::*; + #[cfg(not(feature = "std"))] + use std::{vec, vec::Vec}; + + #[cfg(all(feature = "std", not(feature = "loom")))] + use std::thread; + + #[cfg(all(feature = "std", feature = "loom", not(miri)))] + use loom::thread; + + fn create_pool(cap: usize) -> Pool> { + Pool::bounded(cap, Vec::new, |val| { + val.clear(); + }) + } + + fn basic_get_and_put_in() { + let pool = create_pool(10); + + // Get a new object from the pool + let mut obj = pool.get(); + assert_eq!(*obj, Vec::new()); + + // Modify and return the object + obj.push(42); + drop(obj); + + // Get the object back from the pool + let obj = pool.get(); + assert_eq!(*obj, Vec::new()); + } + #[test] - fn test_works() { - assert_eq!(it_works(), 4); + fn basic_get_and_put() { + #[cfg(feature = "loom")] + loom::model(basic_get_and_put_in); + + #[cfg(not(feature = "loom"))] + basic_get_and_put_in(); + } + + fn get_or_else_in() { + let pool = create_pool(10); + + // Get an object from the pool with a fallback + let mut obj = pool.get_or_else(|| vec![42]); + assert_eq!(*obj, [42]); + + // Modify and return the object + obj.push(43); + drop(obj); + + // Get the object back from the pool with a fallback + let obj = pool.get_or_else(|| vec![42]); + assert_eq!(*obj, []); + + let _objs = (0..10) + .map(|_| pool.get_or_else(|| vec![42])) + .collect::>(); + + let obj = pool.get_or_else(|| vec![42]); + assert_eq!(*obj, [42]); + } + + #[test] + fn get_or_else() { + #[cfg(feature = "loom")] + loom::model(get_or_else_in); + + #[cfg(not(feature = "loom"))] + get_or_else_in(); + } + + fn pool_clone_in() { + let pool = create_pool(10); + let pool_clone = pool.clone(); + + // Get an object from the cloned pool + let mut obj = pool_clone.get(); + assert_eq!(*obj, []); + + // Modify and return the object + obj.push(42); + drop(obj); + + // Get the object back from the original pool + let obj = pool.get(); + assert_eq!(*obj, []); + } + + #[test] + fn pool_clone() { + #[cfg(feature = "loom")] + loom::model(pool_clone_in); + + #[cfg(not(feature = "loom"))] + pool_clone_in(); + } + + #[cfg(feature = "std")] + fn multi_threaded_access_in() { + #[cfg(not(any(feature = "loom", miri)))] + const OUTER: usize = 10; + #[cfg(any(feature = "loom", miri))] + const OUTER: usize = 2; + + #[cfg(not(any(feature = "loom", miri)))] + const INNER: usize = 100; + #[cfg(any(feature = "loom", miri))] + const INNER: usize = 10; + + let pool = create_pool(10); + + let mut handles = vec![]; + + for _ in 0..OUTER { + let pool = pool.clone(); + let handle = thread::spawn(move || { + for i in 0..INNER { + let mut obj = pool.get(); + obj.push(i as u8); + drop(obj); + } + }); + handles.push(handle); + } + + for handle in handles { + handle.join().expect("Thread panicked"); + } + + // Check that the pool is still functional after multi-threaded access + let obj = pool.get(); + assert_eq!(*obj, []); + } + + #[test] + #[cfg(feature = "std")] + fn multi_threaded_access() { + #[cfg(all(feature = "std", not(feature = "loom")))] + multi_threaded_access_in(); + + #[cfg(all(feature = "std", feature = "loom"))] + loom::model(multi_threaded_access_in); + } + + fn custom_new_and_reset_in() { + let pool = Pool::bounded( + 10, + || 100, // new closure that creates an i32 with value 100 + |val: &mut i32| { + *val = 200; + }, // reset closure that resets the value to 200 + ); + + // Get a new object from the pool + let mut obj = pool.get(); + assert_eq!(*obj, 100); + + // Modify and return the object + *obj = 42; + drop(obj); + + // Get the object back from the pool + let obj = pool.get(); + assert_eq!(*obj, 200); + } + + #[test] + fn custom_new_and_reset() { + #[cfg(feature = "loom")] + loom::model(custom_new_and_reset_in); + + #[cfg(not(feature = "loom"))] + custom_new_and_reset_in(); + } + + #[cfg(not(feature = "loom"))] + fn stress_test_in() { + let pool = create_pool(10); + + for _ in 0..1_000_000 { + let mut obj = pool.get(); + obj.push(42); + } + + // Check that the pool is still functional after stress test + let obj = pool.get(); + assert_eq!(*obj, []); + } + + #[test] + #[cfg(not(feature = "loom"))] + fn stress_test() { + stress_test_in(); + } + + fn test_reusable_object_in() { + let pool = create_pool(10); + + { + let mut obj = pool.get(); + obj.push(42); + assert_eq!(*obj, [42]); + // obj goes out of scope and is returned to the pool + } + + // Get the object back from the pool + let obj = pool.get(); + assert_eq!(*obj, []); + } + + #[test] + fn test_reusable_object() { + #[cfg(feature = "loom")] + loom::model(test_reusable_object_in); + + #[cfg(not(feature = "loom"))] + test_reusable_object_in(); + } + + fn test_reset_on_put_in() { + let pool = create_pool(10); + + let mut obj = pool.get(); + obj.push(123); + drop(obj); // Object is returned to the pool and reset + + // Get the object back from the pool + let obj = pool.get(); + assert_eq!(*obj, []); // Ensure that the object was reset + } + + #[test] + fn test_reset_on_put() { + #[cfg(feature = "loom")] + loom::model(test_reset_on_put_in); + + #[cfg(not(feature = "loom"))] + test_reset_on_put_in(); + } + + fn test_as_ref_in() { + let pool = create_pool(10); + + let mut obj = pool.get(); + obj.push(42); + + { + let obj_ref = obj.as_ref(); + assert_eq!(*obj_ref, [42]); + } + + { + let obj_mut = obj.as_mut(); + obj_mut.push(43); + } + + let mut obj = pool.get_owned(); + obj.push(42); + { + let obj_ref = obj.as_ref(); + assert_eq!(*obj_ref, [42]); + } + + { + let obj_mut = obj.as_mut(); + obj_mut.push(43); + } + } + + #[test] + fn test_as_ref() { + #[cfg(feature = "loom")] + loom::model(test_as_ref_in); + + #[cfg(not(feature = "loom"))] + test_as_ref_in(); } } diff --git a/tests/foo.rs b/tests/foo.rs deleted file mode 100644 index 8b13789..0000000 --- a/tests/foo.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/test_bounded.rs b/tests/test_bounded.rs new file mode 100644 index 0000000..b40b2ee --- /dev/null +++ b/tests/test_bounded.rs @@ -0,0 +1,20 @@ +use objectpool::Pool; + +#[macro_use] +mod test_generic; + +fn make_pool() -> Pool { + Pool::::bounded(10, Default::default, |v| { + *v = 0; + }) +} + +#[cfg(not(feature = "loom"))] +fn make_recycle_pool() -> Pool { + Pool::::bounded(10, Default::default, |_v| {}) +} + +test_generic_01!(test_bounded_01, make_pool()); +test_generic_02!(test_bounded_02, make_pool()); +#[cfg(not(feature = "loom"))] +test_recycle_generic_01!(test_bounded_recycle_01, make_recycle_pool()); diff --git a/tests/test_generic.rs b/tests/test_generic.rs new file mode 100644 index 0000000..52e6338 --- /dev/null +++ b/tests/test_generic.rs @@ -0,0 +1,91 @@ +#[macro_export] +macro_rules! test_generic_01 { + ($name:ident, $expression:expr) => { + #[test] + fn $name() { + fn run() { + let pool = $expression; + for _ in 0..2 { + let mut v = pool.get(); + assert_eq!(*v, 0); + *v += 1; + } + } + + #[cfg(not(feature = "loom"))] + run(); + + #[cfg(feature = "loom")] + loom::model(run); + } + }; +} + +#[macro_export] +macro_rules! test_generic_02 { + ($name:ident, $expression:expr) => { + #[test] + #[cfg(not(feature = "loom"))] + fn $name() { + use std::sync::mpsc; + use std::sync::Arc; + use std::thread; + + let pool = Arc::new($expression); + + let (tx, rx) = mpsc::channel(); + let mut children = Vec::new(); + + for id in 0..5 { + let thread_tx = tx.clone(); + let thread_pool = Arc::clone(&pool); + + let child = thread::spawn(move || { + let mut msg = thread_pool.get(); + *msg = id; + thread_tx.send(*msg).unwrap(); + }); + children.push(child); + } + + let mut msgs = Vec::new(); + for _ in 0..5 { + let msg = rx.recv().unwrap(); + if !msgs.contains(&msg) && msg < 5 { + msgs.push(msg); + } + } + assert_eq!(msgs.len(), 5); + + for child in children { + child.join().unwrap(); + } + } + }; +} + +#[macro_export] +macro_rules! test_recycle_generic_01 { + ($name:ident, $expression:expr) => { + #[test] + #[cfg(not(feature = "loom"))] + fn $name() { + fn run() { + let pool = $expression; + + let mut item1 = pool.get(); + *item1 = 5; + drop(item1); + + let item2 = pool.get(); + assert_eq!(*item2, 5); + } + + #[cfg(not(feature = "loom"))] + run(); + + #[cfg(feature = "loom")] + loom::model(run); + } + }; +} diff --git a/tests/test_unbounded.rs b/tests/test_unbounded.rs new file mode 100644 index 0000000..7790365 --- /dev/null +++ b/tests/test_unbounded.rs @@ -0,0 +1,20 @@ +use objectpool::Pool; + +#[macro_use] +mod test_generic; + +fn make_pool() -> Pool { + Pool::::unbounded(Default::default, |v| { + *v = 0; + }) +} + +#[cfg(not(feature = "loom"))] +fn make_recycle_pool() -> Pool { + Pool::::unbounded(Default::default, |_v| {}) +} + +test_generic_01!(test_unbounded_01, make_pool()); +test_generic_02!(test_unbounded_02, make_pool()); +#[cfg(not(feature = "loom"))] +test_recycle_generic_01!(test_unbounded_recycle_01, make_recycle_pool());