11 unstable releases (3 breaking)
0.4.3 | Aug 9, 2020 |
---|---|
0.4.2 | Apr 24, 2020 |
0.3.1 | Apr 23, 2020 |
0.3.0 | Mar 10, 2020 |
0.1.0 | Nov 26, 2019 |
#188 in Memory management
24,967 downloads per month
Used in 9 crates
(8 directly)
69KB
1.5K
SLoC
refpool
A reimplementation of Rust's std::boxed::Box
and std::rc::Rc
which uses a pool of reusable memory
to speed up reallocation.
Is It Fast?
It's about twice as fast as the system allocator on Linux systems, and six times as fast on Windows systems, when the pool is non-empty. For certain data types, gains can be even higher.
Documentation
Licence
Copyright 2019 Bodil Stokke
This software is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
Code of Conduct
Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.
lib.rs
:
A reimplementation of std::boxed::Box
and std::rc::Rc
which uses a pool of reusable memory to speed up reallocation.
Prerequisites
In order to initialise a type to its default value from the memory pool
using PoolBox::default()
or
PoolRef::default()
, it needs to implement
PoolDefault
.
If you want to be able to use PoolRef::make_mut()
, it
also needs to implement PoolClone
.
For constructing values using PoolRef::new()
, there's no
requirement.
There are implementations for PoolDefault
and
PoolClone
for most primitive types and a good selection of
std
's data types, and you can easily provide default implementations for
your own types by implementing the marker trait
PoolDefaultImpl
. You can also implement your own if you
have data structures whose memory doesn't need to be fully intitialised at
construction time, which can give you a slight performance boost. (This
optimisation is why PoolDefault
and
PoolClone
exist as distinct traits, otherwise
Default
and Clone
would have sufficed.)
Usage
You create new values by calling
PoolRef::default(pool)
or PoolRef::new(pool, value)
. This will use memory from the pool if available,
falling back to a normal heap allocation if the pool is empty. When the
last PoolRef
referencing the value is dropped, its allocated
memory is returned to the pool.
Differences from Box
and Rc
PoolBox
is API compatible with Box
and PoolRef
with Rc
, with the following exceptions:
- Types handled by the pool must be
Sized
. This means the pool won't accept trait objects, ie. noPool<dyn A>
. - Constructors need a
Pool
argument, so they're necessarily different: instead ofRc::new(value)
, you havePoolRef::default(pool)
to construct a default value andPoolRef::new(pool, value)
as the equivalent ofRc::new(value)
. Likewise forPoolBox
. PoolBox
andPoolRef
do not implementDefault
, because you need aPool
argument to construct an instance. UsePoolRef::default(pool)
.- There's currently no equivalent to
Weak
forPoolRef
. - Experimental APIs are not implemented.
Thread Safety
Pool
is strictly thread local, ie. it does not
implement Sync
and it will fail in appalling ways if you still
somehow manage to access it from two different threads. There is no
equivalent of Arc
because adding thread safety to the pool turns
out to degrade performance sufficiently that the pool is no longer providing
a significant performance benefit even with the slowest system allocators
you're likely to come across in the wild (by which I mean Windows).
Performance
You can expect Pool
to always outperform the system allocator,
though the performance gains will vary between platforms. Preliminary
benchmarks show it's approximately twice as fast on Linux, and 5-6 times as
fast on Windows. Custom allocators like jemalloc may yield even less
benefit, but it's very unlikely you'll find an allocator that can outperform
the pool.
You can expect bigger performance gains from data types with beneficial
PoolDefault
and PoolClone
implementations,
"beneficial" in this case meaning cases where you can leave most of the
allocated memory uninitialised. sized_chunks::Chunk
, which
allocates 528 bytes on 64-bit platforms but only needs to initialise 16
of them for PoolDefault
, would be a good example of this.
Example
// Create a pool of `usize` with a max size of 1 (for argument's sake).
let mut pool: Pool<usize> = Pool::new(1);
{
// Create a reference handle to a usize initialised to 0.
// The pool starts out empty, so this triggers a normal heap alloc.
let value_ref = PoolRef::default(&mut pool);
assert_eq!(0, *value_ref); // You can deref it just like `Rc`.
} // `value_ref` is dropped here, and its heap memory goes on the pool.
// Check that we do indeed have one allocation in the pool now.
assert_eq!(1, pool.get_pool_size());
// Create another reference and initialise it to 31337, a good round number.
// This will reuse `value_ref`'s memory.
let another_value_ref = PoolRef::new(&mut pool, 31337);
assert_eq!(31337, *another_value_ref);
// Check that the pool is again empty after we reused the previous memory.
assert_eq!(0, pool.get_pool_size());
Feature Flags
There's one feature flag available, default_impl
, which requires a nightly
rustc because it leans on the min_specialization
language feature, which
removes the PoolDefaultImpl
trait and instead provides a default
overridable implementation for PoolClone
and PoolDefault
for any type
that implements Clone
and Default
. PoolDefaultImpl
is an unfortunate
hack to get around the current absence of specialisation in stable rustc.