Skip to content

Commit

Permalink
more impl + comments
Browse files Browse the repository at this point in the history
  • Loading branch information
liss-h committed Jan 6, 2025
1 parent d8463fb commit e57bf32
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 58 deletions.
188 changes: 131 additions & 57 deletions include/dice/template-library/pool_allocator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,27 @@

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`.
*
* @tparam bucket_sizes allocation sizes (in bytes) for the underlying arenas
*/
template<size_t ...bucket_sizes>
struct pool_allocator_state {
private:
std::array<boost::pool<>, sizeof...(bucket_sizes)> pools;

template<size_t ix, size_t bucket_size, size_t ...rest>
void *allocate_impl(size_t n_bytes) {
if (n_bytes < bucket_size) {
void *ptr = pools[ix].malloc();
if (ptr == nullptr) [[unlikely]] {
throw std::bad_alloc{};
}
return ptr;
}

if constexpr (sizeof...(rest) > 0) {
return allocate_impl<ix + 1, rest...>(n_bytes);
} else {
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) {
pools[ix].free(data);
}

if constexpr (sizeof...(rest) > 0) {
return deallocate_impl<ix + 1, rest...>(data, n_bytes);
} else {
return delete[] static_cast<char *>(data);
}
}

public:
void *allocate(size_t n_bytes) {
return allocate_impl<0, bucket_sizes...>(n_bytes);
}

void deallocate(void *data, size_t n_bytes) {
return deallocate_impl<0, bucket_sizes...>(data, n_bytes);
}
};

template<typename T, 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 *;
Expand All @@ -63,9 +38,9 @@ namespace dice::template_library {
using size_type = size_t;
using difference_type = std::ptrdiff_t;

/*using propagate_on_container_copy_assignment = typename std::allocator_traits<upstream_allocator_type>::propagate_on_container_copy_assignment;
using propagate_on_container_move_assignment = typename std::allocator_traits<upstream_allocator_type>::propagate_on_container_move_assignment;
using propagate_on_container_swap = typename std::allocator_traits<upstream_allocator_type>::propagate_on_container_swap;*/
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>
Expand All @@ -77,12 +52,11 @@ namespace dice::template_library {
template<typename, size_t ...>
friend struct pool_allocator;

std::shared_ptr<pool_allocator_state<bucket_sizes...>> state_;

pool<bucket_sizes...> *pool_;

public:
constexpr pool_allocator()
: state_{std::make_shared<pool_allocator_state<bucket_sizes...>>()} {
explicit constexpr pool_allocator(pool<bucket_sizes...> &p) noexcept
: pool_{&p} {
}

constexpr pool_allocator(pool_allocator const &other) noexcept = default;
Expand All @@ -92,30 +66,130 @@ namespace dice::template_library {

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

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

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

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

friend constexpr void swap(pool_allocator &a, pool_allocator &b) noexcept {
using std::swap;
swap(a.state_, b.state_);
swap(a.pool_, b.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(std::ranges::is_sorted(std::array{bucket_sizes...}),
"bucket_sizes parameters must be sorted (small to large)");

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

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_;

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);
}

if constexpr (sizeof...(rest) > 0) {
return deallocate_impl<ix + 1, rest...>(data, n_bytes);
} else {
// does not fit into any bucket, must have been allocated via new[]
return 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]] pool_allocator<T, bucket_sizes...> get_allocator() noexcept {
return pool_allocator<T, bucket_sizes...>{*this};
}
};

} // namespace dice::template_library


Expand Down
7 changes: 6 additions & 1 deletion tests/tests_pool_allocator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@
#include <dice/template-library/pool_allocator.hpp>

TEST_SUITE("pool allocator") {
// TODO
TEST_CASE("sanity check") {
dice::template_library::pool<8, 16> pool;



}
}

0 comments on commit e57bf32

Please sign in to comment.