From d8463fb49c7cae0805ba41c517790e3a9f1c2cf5 Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:21:43 +0100 Subject: [PATCH 01/14] WIP --- .../dice/template-library/pool_allocator.hpp | 122 ++++++++++++++++++ tests/CMakeLists.txt | 3 + tests/tests_pool_allocator.cpp | 8 ++ 3 files changed, 133 insertions(+) create mode 100644 include/dice/template-library/pool_allocator.hpp create mode 100644 tests/tests_pool_allocator.cpp diff --git a/include/dice/template-library/pool_allocator.hpp b/include/dice/template-library/pool_allocator.hpp new file mode 100644 index 0000000..6c8e7c4 --- /dev/null +++ b/include/dice/template-library/pool_allocator.hpp @@ -0,0 +1,122 @@ +#ifndef DICE_TEMPLATELIBRARY_POOLALLOCATOR_HPP +#define DICE_TEMPLATELIBRARY_POOLALLOCATOR_HPP + +#include + +#include +#include + +namespace dice::template_library { + + template + struct pool_allocator_state { + private: + std::array, sizeof...(bucket_sizes)> pools; + + template + 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(n_bytes); + } else { + return new char[n_bytes]; + } + } + + template + 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(data, n_bytes); + } else { + return delete[] static_cast(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 + 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 = typename std::allocator_traits::propagate_on_container_copy_assignment; + using propagate_on_container_move_assignment = typename std::allocator_traits::propagate_on_container_move_assignment; + using propagate_on_container_swap = typename std::allocator_traits::propagate_on_container_swap;*/ + using is_always_equal = std::false_type; + + template + struct rebind { + using other = pool_allocator; + }; + + private: + template + friend struct pool_allocator; + + std::shared_ptr> state_; + + + public: + constexpr pool_allocator() + : state_{std::make_shared>()} { + } + + constexpr pool_allocator(pool_allocator const &other) noexcept = default; + constexpr pool_allocator(pool_allocator &&other) noexcept = default; + constexpr pool_allocator &operator=(pool_allocator const &other) noexcept = default; + constexpr pool_allocator &operator=(pool_allocator &&other) noexcept = default; + + template + constexpr pool_allocator(pool_allocator const &other) noexcept + : state_{other.state_} { + } + + constexpr pointer allocate(size_t n) { + return state_->allocate(sizeof(T) * n); + } + + constexpr void deallocate(pointer ptr, size_t n) { + state_->deallocate(ptr, sizeof(T) * n); + } + + constexpr pool_allocator select_on_container_copy_construction() const { + return pool_allocator{state_}; + } + + friend constexpr void swap(pool_allocator &a, pool_allocator &b) noexcept { + using std::swap; + swap(a.state_, b.state_); + } + + 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 63181f4..0799ac2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -63,3 +63,6 @@ custom_add_test(tests_variant2) add_executable(tests_type_traits tests_type_traits.cpp) custom_add_test(tests_type_traits) + +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..62f01f9 --- /dev/null +++ b/tests/tests_pool_allocator.cpp @@ -0,0 +1,8 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +#include + +TEST_SUITE("pool allocator") { + // TODO +} From e57bf329b798e19c95c753a30cd8813c6fabda85 Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:52:22 +0100 Subject: [PATCH 02/14] more impl + comments --- .../dice/template-library/pool_allocator.hpp | 188 ++++++++++++------ tests/tests_pool_allocator.cpp | 7 +- 2 files changed, 137 insertions(+), 58 deletions(-) diff --git a/include/dice/template-library/pool_allocator.hpp b/include/dice/template-library/pool_allocator.hpp index 6c8e7c4..2953cb5 100644 --- a/include/dice/template-library/pool_allocator.hpp +++ b/include/dice/template-library/pool_allocator.hpp @@ -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 - struct pool_allocator_state { - private: - std::array, sizeof...(bucket_sizes)> pools; - - template - 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(n_bytes); - } else { - return new char[n_bytes]; - } - } - - template - 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(data, n_bytes); - } else { - return delete[] static_cast(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 + 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` + */ + template struct pool_allocator { using value_type = T; using pointer = T *; @@ -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::propagate_on_container_copy_assignment; - using propagate_on_container_move_assignment = typename std::allocator_traits::propagate_on_container_move_assignment; - using propagate_on_container_swap = typename std::allocator_traits::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 @@ -77,12 +52,11 @@ namespace dice::template_library { template friend struct pool_allocator; - std::shared_ptr> state_; - + pool *pool_; public: - constexpr pool_allocator() - : state_{std::make_shared>()} { + explicit constexpr pool_allocator(pool &p) noexcept + : pool_{&p} { } constexpr pool_allocator(pool_allocator const &other) noexcept = default; @@ -92,30 +66,130 @@ namespace dice::template_library { template constexpr pool_allocator(pool_allocator 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 + 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; + + 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); + } + + if constexpr (sizeof...(rest) > 0) { + return deallocate_impl(data, n_bytes); + } else { + // does not fit into any bucket, must have been allocated via new[] + return 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); + } + + /** + * 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 + [[nodiscard]] pool_allocator get_allocator() noexcept { + return pool_allocator{*this}; + } + }; + } // namespace dice::template_library diff --git a/tests/tests_pool_allocator.cpp b/tests/tests_pool_allocator.cpp index 62f01f9..d44e058 100644 --- a/tests/tests_pool_allocator.cpp +++ b/tests/tests_pool_allocator.cpp @@ -4,5 +4,10 @@ #include TEST_SUITE("pool allocator") { - // TODO + TEST_CASE("sanity check") { + dice::template_library::pool<8, 16> pool; + + + + } } From 98284e6104b0f46f9584bcd35ccba3591310d90e Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:53:20 +0100 Subject: [PATCH 03/14] fixes and sanity check --- .../dice/template-library/pool_allocator.hpp | 24 +++++++++++-------- tests/tests_pool_allocator.cpp | 16 ++++++++++++- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/include/dice/template-library/pool_allocator.hpp b/include/dice/template-library/pool_allocator.hpp index 2953cb5..c71e7b1 100644 --- a/include/dice/template-library/pool_allocator.hpp +++ b/include/dice/template-library/pool_allocator.hpp @@ -3,8 +3,10 @@ #include +#include #include -#include +#include +#include namespace dice::template_library { @@ -55,14 +57,15 @@ namespace dice::template_library { pool *pool_; public: - explicit constexpr pool_allocator(pool &p) noexcept - : pool_{&p} { + explicit constexpr pool_allocator(pool &parent_pool) noexcept + : pool_{&parent_pool} { } constexpr pool_allocator(pool_allocator const &other) noexcept = default; constexpr pool_allocator(pool_allocator &&other) noexcept = default; constexpr pool_allocator &operator=(pool_allocator const &other) noexcept = default; constexpr pool_allocator &operator=(pool_allocator &&other) noexcept = default; + constexpr ~pool_allocator() noexcept = default; template constexpr pool_allocator(pool_allocator const &other) noexcept @@ -70,7 +73,7 @@ namespace dice::template_library { } constexpr pointer allocate(size_t n) { - return pool_->allocate(sizeof(T) * n); + return static_cast(pool_->allocate(sizeof(T) * n)); } constexpr void deallocate(pointer ptr, size_t n) { @@ -81,9 +84,9 @@ namespace dice::template_library { return pool_allocator{*pool_}; } - friend constexpr void swap(pool_allocator &a, pool_allocator &b) noexcept { + friend constexpr void swap(pool_allocator &lhs, pool_allocator &rhs) noexcept { using std::swap; - swap(a.pool_, b.pool_); + swap(lhs.pool_, rhs.pool_); } bool operator==(pool_allocator const &other) const noexcept = default; @@ -108,7 +111,7 @@ namespace dice::template_library { template void *allocate_impl(size_t n_bytes) { - if (n_bytes < bucket_size) { + if (n_bytes <= bucket_size) { // fits into bucket void *ptr = pools_[ix].malloc(); @@ -129,16 +132,17 @@ namespace dice::template_library { template void deallocate_impl(void *data, size_t n_bytes) { - if (n_bytes < bucket_size) { + if (n_bytes <= bucket_size) { // fits into bucket pools_[ix].free(data); + return; } if constexpr (sizeof...(rest) > 0) { - return deallocate_impl(data, n_bytes); + deallocate_impl(data, n_bytes); } else { // does not fit into any bucket, must have been allocated via new[] - return delete[] static_cast(data); + delete[] static_cast(data); } } diff --git a/tests/tests_pool_allocator.cpp b/tests/tests_pool_allocator.cpp index d44e058..cf2b64f 100644 --- a/tests/tests_pool_allocator.cpp +++ b/tests/tests_pool_allocator.cpp @@ -3,11 +3,25 @@ #include +#include +#include +#include + TEST_SUITE("pool allocator") { TEST_CASE("sanity check") { dice::template_library::pool<8, 16> pool; + auto alloc1 = pool.get_allocator(); // first pool + auto alloc2 = pool.get_allocator>(); // second pool + auto alloc3 = pool.get_allocator>(); // 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); + } } } From f231607958bed0b06c604edba354468d3154d05b Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Tue, 7 Jan 2025 10:10:06 +0100 Subject: [PATCH 04/14] example readme & fix --- README.md | 6 +++ examples/CMakeLists.txt | 6 +++ examples/example_pool_allocator.cpp | 38 +++++++++++++++++++ .../dice/template-library/pool_allocator.hpp | 2 +- 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 examples/example_pool_allocator.cpp 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..61f3f7a --- /dev/null +++ b/examples/example_pool_allocator.cpp @@ -0,0 +1,38 @@ +#include + +#include +#include +#include + +struct list { + uint64_t elem; + list *next; +}; + +int main() { + dice::template_library::pool pool; + + { // efficient pool allocations for elements of known size + auto list_alloc = pool.get_allocator(); + + 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(pool.get_allocator()); + vec.resize(1024); + } +} diff --git a/include/dice/template-library/pool_allocator.hpp b/include/dice/template-library/pool_allocator.hpp index c71e7b1..82c925e 100644 --- a/include/dice/template-library/pool_allocator.hpp +++ b/include/dice/template-library/pool_allocator.hpp @@ -69,7 +69,7 @@ namespace dice::template_library { template constexpr pool_allocator(pool_allocator const &other) noexcept - : pool_{other.state_} { + : pool_{other.pool_} { } constexpr pointer allocate(size_t n) { From 29663a506ed1ca73098be3df8e8331551a67b00e Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Mon, 13 Jan 2025 16:34:04 +0100 Subject: [PATCH 05/14] fix deduction --- include/dice/template-library/pool_allocator.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/dice/template-library/pool_allocator.hpp b/include/dice/template-library/pool_allocator.hpp index 82c925e..7e45fb9 100644 --- a/include/dice/template-library/pool_allocator.hpp +++ b/include/dice/template-library/pool_allocator.hpp @@ -95,7 +95,7 @@ namespace dice::template_library { template struct pool { - static_assert(std::ranges::is_sorted(std::array{bucket_sizes...}), + static_assert(std::ranges::is_sorted(std::array{bucket_sizes...}), "bucket_sizes parameters must be sorted (small to large)"); using size_type = size_t; From 14634a1889f171990745d8deebfa7e5e68d08cc9 Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Mon, 13 Jan 2025 17:00:11 +0100 Subject: [PATCH 06/14] add typedef and check --- include/dice/template-library/pool_allocator.hpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/include/dice/template-library/pool_allocator.hpp b/include/dice/template-library/pool_allocator.hpp index 7e45fb9..c1d06cb 100644 --- a/include/dice/template-library/pool_allocator.hpp +++ b/include/dice/template-library/pool_allocator.hpp @@ -95,12 +95,17 @@ namespace dice::template_library { 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; + template + using corresponding_allocator_type = pool_allocator; + 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 @@ -189,7 +194,7 @@ namespace dice::template_library { * @return `std`-style allocator for this pool */ template - [[nodiscard]] pool_allocator get_allocator() noexcept { + [[nodiscard]] corresponding_allocator_type get_allocator() noexcept { return pool_allocator{*this}; } }; From d36112ee2bb50b8a1649b262cad048c1555407ea Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Mon, 13 Jan 2025 17:07:09 +0100 Subject: [PATCH 07/14] rename typedef --- include/dice/template-library/pool_allocator.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/dice/template-library/pool_allocator.hpp b/include/dice/template-library/pool_allocator.hpp index c1d06cb..ef6f15e 100644 --- a/include/dice/template-library/pool_allocator.hpp +++ b/include/dice/template-library/pool_allocator.hpp @@ -104,7 +104,7 @@ namespace dice::template_library { using difference_type = std::ptrdiff_t; template - using corresponding_allocator_type = pool_allocator; + using allocator_type = pool_allocator; private: // note: underlying allocator can not be specified via template parameter @@ -194,7 +194,7 @@ namespace dice::template_library { * @return `std`-style allocator for this pool */ template - [[nodiscard]] corresponding_allocator_type get_allocator() noexcept { + [[nodiscard]] allocator_type get_allocator() noexcept { return pool_allocator{*this}; } }; From 4b1830c1f0268bab678d427ec75bb0cd7f3dad00 Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:45:45 +0100 Subject: [PATCH 08/14] add some more tests --- tests/tests_pool_allocator.cpp | 70 +++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/tests/tests_pool_allocator.cpp b/tests/tests_pool_allocator.cpp index cf2b64f..8f73858 100644 --- a/tests/tests_pool_allocator.cpp +++ b/tests/tests_pool_allocator.cpp @@ -6,9 +6,38 @@ #include #include #include +#include TEST_SUITE("pool allocator") { - TEST_CASE("sanity check") { + 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<8, 16> pool; auto alloc1 = pool.get_allocator(); // first pool auto alloc2 = pool.get_allocator>(); // second pool @@ -24,4 +53,43 @@ TEST_SUITE("pool allocator") { 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; + 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>>); + + pool_type pool; + allocator_type alloc = pool.get_allocator(); + + 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 + auto alloc3 = allocator_traits::select_on_container_copy_construction(alloc); + + CHECK_EQ(mv, alloc); + CHECK_EQ(alloc, alloc3); + } } From 2083d24b02af120dd63f636d7e258d7deac4b7c2 Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Fri, 17 Jan 2025 09:24:09 +0100 Subject: [PATCH 09/14] clarify chunk vs elem size --- include/dice/template-library/pool_allocator.hpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/include/dice/template-library/pool_allocator.hpp b/include/dice/template-library/pool_allocator.hpp index ef6f15e..f61e91d 100644 --- a/include/dice/template-library/pool_allocator.hpp +++ b/include/dice/template-library/pool_allocator.hpp @@ -18,7 +18,10 @@ namespace dice::template_library { * 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 + * @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 struct pool; From 73d094a78508ab31c66ba97840ba5309e6259a6e Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Fri, 17 Jan 2025 09:40:11 +0100 Subject: [PATCH 10/14] remove unecessary constexpr --- .../dice/template-library/pool_allocator.hpp | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/include/dice/template-library/pool_allocator.hpp b/include/dice/template-library/pool_allocator.hpp index f61e91d..8aa6313 100644 --- a/include/dice/template-library/pool_allocator.hpp +++ b/include/dice/template-library/pool_allocator.hpp @@ -60,34 +60,34 @@ namespace dice::template_library { pool *pool_; public: - explicit constexpr pool_allocator(pool &parent_pool) noexcept + explicit pool_allocator(pool &parent_pool) noexcept : pool_{&parent_pool} { } - constexpr pool_allocator(pool_allocator const &other) noexcept = default; - constexpr pool_allocator(pool_allocator &&other) noexcept = default; - constexpr pool_allocator &operator=(pool_allocator const &other) noexcept = default; - constexpr pool_allocator &operator=(pool_allocator &&other) noexcept = default; - constexpr ~pool_allocator() noexcept = default; + 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 - constexpr pool_allocator(pool_allocator const &other) noexcept + pool_allocator(pool_allocator const &other) noexcept : pool_{other.pool_} { } - constexpr pointer allocate(size_t n) { + pointer allocate(size_t n) { return static_cast(pool_->allocate(sizeof(T) * n)); } - constexpr void deallocate(pointer ptr, size_t n) { + void deallocate(pointer ptr, size_t n) { pool_->deallocate(ptr, sizeof(T) * n); } - constexpr pool_allocator select_on_container_copy_construction() const { + pool_allocator select_on_container_copy_construction() const { return pool_allocator{*pool_}; } - friend constexpr void swap(pool_allocator &lhs, pool_allocator &rhs) noexcept { + friend void swap(pool_allocator &lhs, pool_allocator &rhs) noexcept { using std::swap; swap(lhs.pool_, rhs.pool_); } From 8440df05345f1b894a3e54f52b79c09272a95e07 Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:05:51 +0100 Subject: [PATCH 11/14] add test for rebind alloc --- tests/tests_pool_allocator.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/tests_pool_allocator.cpp b/tests/tests_pool_allocator.cpp index 8f73858..f741506 100644 --- a/tests/tests_pool_allocator.cpp +++ b/tests/tests_pool_allocator.cpp @@ -86,10 +86,15 @@ TEST_SUITE("pool allocator") { cpy = alloc; // copy assignment mv = std::move(cpy); // move assignment swap(mv, alloc); // swap + dice::template_library::pool_allocator const alloc2 = alloc; // converting constructor - auto alloc3 = allocator_traits::select_on_container_copy_construction(alloc); + 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, alloc3); + CHECK_EQ(alloc, alloc4); } } From f5436dd6ee0ba080e402a4960c67f3766c14170f Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:15:31 +0100 Subject: [PATCH 12/14] add capacity comment --- include/dice/template-library/pool_allocator.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/dice/template-library/pool_allocator.hpp b/include/dice/template-library/pool_allocator.hpp index 8aa6313..39f4ba3 100644 --- a/include/dice/template-library/pool_allocator.hpp +++ b/include/dice/template-library/pool_allocator.hpp @@ -21,7 +21,7 @@ namespace dice::template_library { * @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. + * The chunk size itself as well as the maximum capacity cannot be configured, they are automatically determined by boost::pool. */ template struct pool; From 83778aa25f31a16a8c1d9caf002ab9498e4faecc Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Mon, 20 Jan 2025 11:37:24 +0100 Subject: [PATCH 13/14] use shared ptr --- examples/example_pool_allocator.cpp | 7 +- .../dice/template-library/pool_allocator.hpp | 162 +++++++++--------- tests/tests_pool_allocator.cpp | 12 +- 3 files changed, 87 insertions(+), 94 deletions(-) diff --git a/examples/example_pool_allocator.cpp b/examples/example_pool_allocator.cpp index 61f3f7a..cb5c87e 100644 --- a/examples/example_pool_allocator.cpp +++ b/examples/example_pool_allocator.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -10,10 +11,10 @@ struct list { }; int main() { - dice::template_library::pool pool; + dice::template_library::pool_allocator alloc; { // efficient pool allocations for elements of known size - auto list_alloc = pool.get_allocator(); + dice::template_library::pool_allocator list_alloc = alloc; auto *head = list_alloc.allocate(1); // efficient pool allocation new (head) list{.elem = 0, .next = nullptr}; @@ -32,7 +33,7 @@ int main() { } { // fallback allocation with new & support as container allocator - std::vector> vec(pool.get_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 index 39f4ba3..65afc3f 100644 --- a/include/dice/template-library/pool_allocator.hpp +++ b/include/dice/template-library/pool_allocator.hpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace dice::template_library { @@ -23,79 +24,6 @@ namespace dice::template_library { * 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; - - /** - * `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; - - pool *pool_; - - public: - explicit pool_allocator(pool &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 - pool_allocator(pool_allocator const &other) noexcept - : pool_{other.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; - }; - template struct pool { static_assert(sizeof...(bucket_sizes) > 0, @@ -106,9 +34,6 @@ namespace dice::template_library { using size_type = size_t; using difference_type = std::ptrdiff_t; - template - using allocator_type = pool_allocator; - 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 @@ -188,18 +113,87 @@ namespace dice::template_library { 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: /** - * 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 + * Creates a pool_allocator with a default constructed pool */ - template - [[nodiscard]] allocator_type get_allocator() noexcept { - return pool_allocator{*this}; + 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 diff --git a/tests/tests_pool_allocator.cpp b/tests/tests_pool_allocator.cpp index f741506..ff1fb1f 100644 --- a/tests/tests_pool_allocator.cpp +++ b/tests/tests_pool_allocator.cpp @@ -38,10 +38,10 @@ TEST_SUITE("pool allocator") { } TEST_CASE("many allocations and deallocations") { - dice::template_library::pool<8, 16> pool; - auto alloc1 = pool.get_allocator(); // first pool - auto alloc2 = pool.get_allocator>(); // second pool - auto alloc3 = pool.get_allocator>(); // fallback to new + 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); @@ -55,7 +55,6 @@ TEST_SUITE("pool allocator") { } TEST_CASE("allocator interface") { - using pool_type = dice::template_library::pool<8, 16>; using allocator_type = dice::template_library::pool_allocator; using allocator_traits = std::allocator_traits; @@ -73,8 +72,7 @@ TEST_SUITE("pool allocator") { static_assert(std::is_same_v, dice::template_library::pool_allocator>); static_assert(std::is_same_v, std::allocator_traits>>); - pool_type pool; - allocator_type alloc = pool.get_allocator(); + allocator_type alloc; uint64_t *ptr = allocator_traits::allocate(alloc, 1); *ptr = 123; From 918f078e9350fd9510eec198302d50d762c71982 Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Mon, 20 Jan 2025 18:18:33 +0100 Subject: [PATCH 14/14] add test --- tests/tests_pool_allocator.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/tests_pool_allocator.cpp b/tests/tests_pool_allocator.cpp index ff1fb1f..d9783b4 100644 --- a/tests/tests_pool_allocator.cpp +++ b/tests/tests_pool_allocator.cpp @@ -94,5 +94,8 @@ TEST_SUITE("pool allocator") { 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); } }