diff --git a/README.md b/README.md index 81e8c39..0c23cb8 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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). diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 4cdf302..26653ab 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -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 +) diff --git a/examples/example_pool_allocator.cpp b/examples/example_pool_allocator.cpp new file mode 100644 index 0000000..cb5c87e --- /dev/null +++ b/examples/example_pool_allocator.cpp @@ -0,0 +1,39 @@ +#include + +#include +#include +#include +#include + +struct list { + uint64_t elem; + list *next; +}; + +int main() { + dice::template_library::pool_allocator alloc; + + { // efficient pool allocations for elements of known size + dice::template_library::pool_allocator list_alloc = alloc; + + 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> vec(alloc); + vec.resize(1024); + } +} diff --git a/include/dice/template-library/pool_allocator.hpp b/include/dice/template-library/pool_allocator.hpp new file mode 100644 index 0000000..65afc3f --- /dev/null +++ b/include/dice/template-library/pool_allocator.hpp @@ -0,0 +1,202 @@ +#ifndef DICE_TEMPLATELIBRARY_POOLALLOCATOR_HPP +#define DICE_TEMPLATELIBRARY_POOLALLOCATOR_HPP + +#include + +#include +#include +#include +#include +#include + +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 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 as well as the maximum capacity cannot be configured, they are automatically determined by boost::pool. + */ + template + 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{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; + + std::array pools_; + + template + 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(n_bytes); + } else { + // does not fit into any bucket, fall back to new[] + return new char[n_bytes]; + } + } + + template + 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(data, n_bytes); + } else { + // does not fit into any bucket, must have been allocated via new[] + delete[] static_cast(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); + } + }; + + /** + * `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` + */ + template + 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 + struct rebind { + using other = pool_allocator; + }; + + private: + template + friend struct pool_allocator; + + std::shared_ptr> pool_; + + public: + /** + * Creates a pool_allocator with a default constructed pool + */ + pool_allocator() + : pool_{std::make_shared>()} { + } + + explicit pool_allocator(std::shared_ptr> underlying_pool) + : pool_{std::move(underlying_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 + pool_allocator(pool_allocator const &other) noexcept + : pool_{other.pool_} { + } + + [[nodiscard]] std::shared_ptr> const &underlying_pool() const noexcept { + return pool_; + } + + pointer allocate(size_t n) { + return static_cast(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; + }; + +} // namespace dice::template_library + + +#endif // DICE_TEMPLATELIBRARY_POOLALLOCATOR_HPP diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2e004e7..8f39bea 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/tests_pool_allocator.cpp b/tests/tests_pool_allocator.cpp new file mode 100644 index 0000000..d9783b4 --- /dev/null +++ b/tests/tests_pool_allocator.cpp @@ -0,0 +1,101 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +#include + +#include +#include +#include +#include + +TEST_SUITE("pool allocator") { + TEST_CASE("basic pool functions work") { + dice::template_library::pool pool; + + auto *ptr = static_cast(pool.allocate(sizeof(int))); + *ptr = 123; + CHECK_EQ(*ptr, 123); + pool.deallocate(ptr, sizeof(int)); + + auto *ptr2 = static_cast(pool.allocate(sizeof(int))); + CHECK_EQ(ptr, ptr2); + *ptr2 = 456; + CHECK_EQ(*ptr2, 456); + pool.deallocate(ptr2, sizeof(int)); + + auto *ptr3 = static_cast(pool.allocate(sizeof(long))); + CHECK_EQ(static_cast(ptr2), static_cast(ptr3)); + *ptr3 = 678; + CHECK_EQ(*ptr3, 678); + pool.deallocate(ptr3, sizeof(long)); + + auto *ptr4 = static_cast *>(pool.allocate(sizeof(std::array))); + (*ptr4)[0] = 123; + (*ptr4)[1] = 456; + CHECK_EQ((*ptr4)[0], 123); + CHECK_EQ((*ptr4)[1], 456); + pool.deallocate(ptr4, sizeof(std::array)); + } + + TEST_CASE("many allocations and deallocations") { + dice::template_library::pool_allocator alloc; + dice::template_library::pool_allocator alloc1 = alloc; // first pool + dice::template_library::pool_allocator, 8, 16> alloc2 = alloc; // second pool + dice::template_library::pool_allocator, 8, 16> alloc3 = alloc; // 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 allocator_type = dice::template_library::pool_allocator; + using allocator_traits = std::allocator_traits; + + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v, dice::template_library::pool_allocator>); + static_assert(std::is_same_v, std::allocator_traits>>); + + allocator_type alloc; + + 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 const alloc2 = alloc; // converting constructor + allocator_traits::template rebind_alloc const alloc3 = alloc; + + static_assert(std::is_same_v); + CHECK_EQ(alloc2, alloc3); + + auto alloc4 = allocator_traits::select_on_container_copy_construction(alloc); + CHECK_EQ(mv, alloc); + CHECK_EQ(alloc, alloc4); + + allocator_type alloc5{alloc.underlying_pool()}; + CHECK_EQ(alloc5, alloc); + } +}