Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: pool allocator #66

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ It contains:
- `for_{types,values,range}`: Compile time for loops for types, values or ranges
- `polymorphic_allocator`: Like `std::pmr::polymorphic_allocator` but with static dispatch
- `limit_allocator`: Allocator wrapper that limits the amount of memory that is allowed to be allocated
- `pool` & `pool_allocator`: Arena/pool allocator optimized for a limited number of known allocation sizes.
- `DICE_DEFER`/`DICE_DEFER_TO_SUCCES`/`DICE_DEFER_TO_FAIL`: On-the-fly RAII for types that do not support it natively (similar to go's `defer` keyword)
- `overloaded`: Composition for `std::variant` visitor lambdas
- `flex_array`: A combination of `std::array`, `std::span` and a `vector` with small buffer optimization
Expand Down Expand Up @@ -69,6 +70,11 @@ Which means: vtables will not work (because they use absolute pointers) and ther
Allocator wrapper that limits the amount of memory that can be allocated through the inner allocator.
If the limit is exceeded it will throw `std::bad_alloc`.

### `pool_allocator`
A memory arena/pool allocator with configurable allocation sizes. This is implemented
as a collection of pools with varying allocation sizes. Allocations that do not
fit into any of its pools are directly served via `new`.

### `DICE_DEFER`/`DICE_DEFER_TO_SUCCES`/`DICE_DEFER_TO_FAIL`
A mechanism similar to go's `defer` keyword, which can be used to defer some action to scope exit.
The primary use-case for this is on-the-fly RAII-like resource management for types that do not support RAII (for example C types).
Expand Down
6 changes: 6 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,9 @@ target_link_libraries(example_limit_allocator
dice-template-library::dice-template-library
)

add_executable(example_pool_allocator
example_pool_allocator.cpp)
target_link_libraries(example_pool_allocator
PRIVATE
dice-template-library::dice-template-library
)
38 changes: 38 additions & 0 deletions examples/example_pool_allocator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include <dice/template-library/pool_allocator.hpp>

#include <cstdint>
#include <iostream>
#include <vector>

struct list {
uint64_t elem;
list *next;
};

int main() {
dice::template_library::pool<sizeof(list)> pool;

{ // efficient pool allocations for elements of known size
auto list_alloc = pool.get_allocator<list>();

auto *head = list_alloc.allocate(1); // efficient pool allocation
new (head) list{.elem = 0, .next = nullptr};

head->next = list_alloc.allocate(1); // efficient pool allocation
new (head->next) list{.elem = 1, .next = nullptr};

auto const *cur = head;
while (cur != nullptr) {
std::cout << cur->elem << " ";
cur = cur->next;
}

list_alloc.deallocate(head->next, 1);
list_alloc.deallocate(head, 1);
}

{ // fallback allocation with new & support as container allocator
std::vector<uint64_t, dice::template_library::pool_allocator<uint64_t, sizeof(list)>> vec(pool.get_allocator());
vec.resize(1024);
}
}
208 changes: 208 additions & 0 deletions include/dice/template-library/pool_allocator.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#ifndef DICE_TEMPLATELIBRARY_POOLALLOCATOR_HPP
#define DICE_TEMPLATELIBRARY_POOLALLOCATOR_HPP

#include <boost/pool/pool.hpp>

#include <algorithm>
#include <array>
#include <cstddef>
#include <type_traits>

namespace dice::template_library {

/**
* A memory pool or arena that is efficient for allocations which are smaller or equal in size
* for one of `bucket_sizes...`.
*
* The implementation consists of one arena per provided bucket size.
* An allocation will be placed into the first bucket where it can fit.
* Allocations that do not fit into any bucket are fulfilled with calls to `new`.
nkaralis marked this conversation as resolved.
Show resolved Hide resolved
*
* @tparam bucket_sizes allocation sizes for individual elements (in bytes) for the underlying arenas.
* Each size provided here is used to configure the element size of a single arena.
* Importantly, it is **not** the arena chunk size, rather it is the size of elements being placed into the arena.
* The chunk size itself cannot be configured, it is automatically determined by boost::pool.
*/
template<size_t ...bucket_sizes>
struct pool;

/**
* `std`-style allocator that allocates into an underlying pool.
* The bucket size used for allocation is `sizeof(T) * n_elems`.
*
* @tparam T type to be allocated
* @tparam bucket_sizes same as for `pool<bucket_sizes...>`
*/
template<typename T, size_t ...bucket_sizes>
struct pool_allocator {
using value_type = T;
using pointer = T *;
using const_pointer = T const *;
using void_pointer = void *;
using const_void_pointer = void const *;
using size_type = size_t;
using difference_type = std::ptrdiff_t;

using propagate_on_container_copy_assignment = std::true_type;
using propagate_on_container_move_assignment = std::true_type;
using propagate_on_container_swap = std::true_type;
using is_always_equal = std::false_type;

template<typename U>
struct rebind {
using other = pool_allocator<U, bucket_sizes...>;
};

private:
template<typename, size_t ...>
friend struct pool_allocator;

pool<bucket_sizes...> *pool_;

public:
explicit pool_allocator(pool<bucket_sizes...> &parent_pool) noexcept
: pool_{&parent_pool} {
}

pool_allocator(pool_allocator const &other) noexcept = default;
pool_allocator(pool_allocator &&other) noexcept = default;
pool_allocator &operator=(pool_allocator const &other) noexcept = default;
pool_allocator &operator=(pool_allocator &&other) noexcept = default;
~pool_allocator() noexcept = default;

template<typename U>
pool_allocator(pool_allocator<U, bucket_sizes...> const &other) noexcept
: pool_{other.pool_} {
}

pointer allocate(size_t n) {
return static_cast<pointer>(pool_->allocate(sizeof(T) * n));
}

void deallocate(pointer ptr, size_t n) {
pool_->deallocate(ptr, sizeof(T) * n);
}

pool_allocator select_on_container_copy_construction() const {
return pool_allocator{*pool_};
}

friend void swap(pool_allocator &lhs, pool_allocator &rhs) noexcept {
using std::swap;
swap(lhs.pool_, rhs.pool_);
}

bool operator==(pool_allocator const &other) const noexcept = default;
bool operator!=(pool_allocator const &other) const noexcept = default;
};

template<size_t ...bucket_sizes>
struct pool {
static_assert(sizeof...(bucket_sizes) > 0,
"must at least provide one bucket size, otherwise this would never use a pool for allocation");
static_assert(std::ranges::is_sorted(std::array<size_t, sizeof...(bucket_sizes)>{bucket_sizes...}),
"bucket_sizes parameters must be sorted (small to large)");

using size_type = size_t;
using difference_type = std::ptrdiff_t;

template<typename T>
using allocator_type = pool_allocator<T, bucket_sizes...>;

private:
// note: underlying allocator can not be specified via template parameter
// because that would be of very limited usefulness, as boost::pool requires the allocation/deallocation functions
// to be `static`
using pool_type = boost::pool<boost::default_user_allocator_new_delete>;

std::array<pool_type, sizeof...(bucket_sizes)> pools_;
nkaralis marked this conversation as resolved.
Show resolved Hide resolved

template<size_t ix, size_t bucket_size, size_t ...rest>
void *allocate_impl(size_t n_bytes) {
if (n_bytes <= bucket_size) {
// fits into bucket

void *ptr = pools_[ix].malloc();
if (ptr == nullptr) [[unlikely]] {
// boost::pool uses null-return instead of exception
throw std::bad_alloc{};
}
return ptr;
}

if constexpr (sizeof...(rest) > 0) {
return allocate_impl<ix + 1, rest...>(n_bytes);
} else {
// does not fit into any bucket, fall back to new[]
return new char[n_bytes];
}
}

template<size_t ix, size_t bucket_size, size_t ...rest>
void deallocate_impl(void *data, size_t n_bytes) {
if (n_bytes <= bucket_size) {
// fits into bucket
pools_[ix].free(data);
return;
}

if constexpr (sizeof...(rest) > 0) {
deallocate_impl<ix + 1, rest...>(data, n_bytes);
} else {
// does not fit into any bucket, must have been allocated via new[]
delete[] static_cast<char *>(data);
}
}

public:
pool() : pools_{pool_type{bucket_sizes}...} {
}

// underlying implementation does not support copying/moving
pool(pool const &other) = delete;
pool(pool &&other) = delete;
pool &operator=(pool const &other) = delete;
pool &operator=(pool &&other) = delete;

~pool() noexcept = default;

/**
* Allocate a chunk of at least `n_bytes` bytes.
* In case `n_bytes` is smaller or equal to any of bucket_sizes..., will be allocated
* in the smallest bucket it fits in, otherwise the allocation will be directly fulfilled via a call to `new`.
*
* @param n_bytes number of bytes to allocate
* @return (non-null) pointer to allocated region
* @throws std::bad_alloc on allocation failure
*/
void *allocate(size_t n_bytes) {
return allocate_impl<0, bucket_sizes...>(n_bytes);
}

/**
* Deallocate a region previously allocated via `pool::allocate`.
*
* @param data pointer to the previously allocated region. Note: data must have been allocated by `*this`
* @param n_bytes size in bytes of the previously allocated region. Note: `n_bytes` must be the same value as was provided for the call to `allocate` that allocated `data`.
*/
void deallocate(void *data, size_t n_bytes) {
return deallocate_impl<0, bucket_sizes...>(data, n_bytes);
}

/**
* Retrieve an (`std`-style) allocator that allocates on `*this` pool.
*
* @warning the pool (`*this`) must always outlive the returned `pool_allocator`
* @tparam T the type that should be allocated by the returned allocator
* @return `std`-style allocator for this pool
*/
template<typename T = std::byte>
[[nodiscard]] allocator_type<T> get_allocator() noexcept {
return pool_allocator<T, bucket_sizes...>{*this};
}
};

} // namespace dice::template_library


#endif // DICE_TEMPLATELIBRARY_POOLALLOCATOR_HPP
3 changes: 3 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,6 @@ custom_add_test(tests_type_traits)

add_executable(tests_limit_allocator tests_limit_allocator.cpp)
custom_add_test(tests_limit_allocator)

add_executable(tests_pool_allocator tests_pool_allocator.cpp)
custom_add_test(tests_pool_allocator)
95 changes: 95 additions & 0 deletions tests/tests_pool_allocator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>

#include <dice/template-library/pool_allocator.hpp>

#include <array>
#include <cstddef>
#include <cstdint>
#include <type_traits>

TEST_SUITE("pool allocator") {
TEST_CASE("basic pool functions work") {
dice::template_library::pool<sizeof(long)> pool;

auto *ptr = static_cast<int *>(pool.allocate(sizeof(int)));
*ptr = 123;
CHECK_EQ(*ptr, 123);
pool.deallocate(ptr, sizeof(int));

auto *ptr2 = static_cast<int *>(pool.allocate(sizeof(int)));
CHECK_EQ(ptr, ptr2);
*ptr2 = 456;
CHECK_EQ(*ptr2, 456);
pool.deallocate(ptr2, sizeof(int));

auto *ptr3 = static_cast<long *>(pool.allocate(sizeof(long)));
CHECK_EQ(static_cast<void *>(ptr2), static_cast<void *>(ptr3));
*ptr3 = 678;
CHECK_EQ(*ptr3, 678);
pool.deallocate(ptr3, sizeof(long));

auto *ptr4 = static_cast<std::array<long, 2> *>(pool.allocate(sizeof(std::array<long, 2>)));
(*ptr4)[0] = 123;
(*ptr4)[1] = 456;
CHECK_EQ((*ptr4)[0], 123);
CHECK_EQ((*ptr4)[1], 456);
pool.deallocate(ptr4, sizeof(std::array<long, 2>));
}

TEST_CASE("many allocations and deallocations") {
dice::template_library::pool<8, 16> pool;
auto alloc1 = pool.get_allocator<uint64_t>(); // first pool
auto alloc2 = pool.get_allocator<std::array<uint64_t, 2>>(); // second pool
auto alloc3 = pool.get_allocator<std::array<uint64_t, 4>>(); // fallback to new

for (size_t ix = 0; ix < 1'000'000; ++ix) {
auto *ptr1 = alloc1.allocate(1);
auto *ptr2 = alloc2.allocate(1);
auto *ptr3 = alloc3.allocate(1);

alloc2.deallocate(ptr2, 1);
alloc3.deallocate(ptr3, 1);
alloc1.deallocate(ptr1, 1);
}
}

TEST_CASE("allocator interface") {
using pool_type = dice::template_library::pool<8, 16>;
using allocator_type = dice::template_library::pool_allocator<uint64_t, 8, 16>;
using allocator_traits = std::allocator_traits<allocator_type>;

static_assert(std::is_same_v<typename allocator_traits::value_type, uint64_t>);
static_assert(std::is_same_v<typename allocator_traits::pointer, uint64_t *>);
static_assert(std::is_same_v<typename allocator_traits::const_pointer, uint64_t const *>);
static_assert(std::is_same_v<typename allocator_traits::void_pointer, void *>);
static_assert(std::is_same_v<typename allocator_traits::const_void_pointer, void const *>);
static_assert(std::is_same_v<typename allocator_traits::difference_type, std::ptrdiff_t>);
static_assert(std::is_same_v<typename allocator_traits::size_type, size_t>);
static_assert(std::is_same_v<typename allocator_traits::propagate_on_container_copy_assignment, std::true_type>);
static_assert(std::is_same_v<typename allocator_traits::propagate_on_container_move_assignment, std::true_type>);
static_assert(std::is_same_v<typename allocator_traits::propagate_on_container_swap, std::true_type>);
static_assert(std::is_same_v<typename allocator_traits::is_always_equal, std::false_type>);
static_assert(std::is_same_v<typename allocator_traits::template rebind_alloc<int64_t>, dice::template_library::pool_allocator<int64_t, 8, 16>>);
static_assert(std::is_same_v<typename allocator_traits::template rebind_traits<int64_t>, std::allocator_traits<dice::template_library::pool_allocator<int64_t, 8, 16>>>);

pool_type pool;
allocator_type alloc = pool.get_allocator<uint64_t>();

uint64_t *ptr = allocator_traits::allocate(alloc, 1);
*ptr = 123;
CHECK_EQ(*ptr, 123);
allocator_traits::deallocate(alloc, ptr, 1);

auto cpy = alloc; // copy ctor
auto mv = std::move(cpy); // move ctor
cpy = alloc; // copy assignment
mv = std::move(cpy); // move assignment
swap(mv, alloc); // swap
dice::template_library::pool_allocator<int, 8, 16> const alloc2 = alloc; // converting constructor
auto alloc3 = allocator_traits::select_on_container_copy_construction(alloc);

CHECK_EQ(mv, alloc);
CHECK_EQ(alloc, alloc3);
}
liss-h marked this conversation as resolved.
Show resolved Hide resolved
}
Loading