From 76cb53e0e293537cd9b19d6e88a42d2f0481080e Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:41:05 +0100 Subject: [PATCH 1/4] add limit_allocator --- README.md | 5 + examples/CMakeLists.txt | 7 + examples/example_limit_allocator.cpp | 26 +++ .../dice/template-library/limit_allocator.hpp | 174 ++++++++++++++++++ tests/CMakeLists.txt | 3 + tests/tests_limit_allocator.cpp | 39 ++++ 6 files changed, 254 insertions(+) create mode 100644 examples/example_limit_allocator.cpp create mode 100644 include/dice/template-library/limit_allocator.hpp create mode 100644 tests/tests_limit_allocator.cpp diff --git a/README.md b/README.md index 22c5724..81e8c39 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ It contains: - `integral_template_variant`: A wrapper type for `std::variant` guarantees to only contain variants of the form `T` where $\texttt{ix}\in [\texttt{first},\texttt{last}]$ (inclusive). - `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 - `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 @@ -64,6 +65,10 @@ The problem with `mmap` allocations is that they will be placed at an arbitrary therefore absolute pointers will cause segfaults if the segment is reloaded. Which means: vtables will not work (because they use absolute pointers) and therefore you cannot use `std::pmr::polymorphic_allocator`. +### `limit_allocator` +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`. + ### `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 6a98147..4cdf302 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -80,3 +80,10 @@ target_link_libraries(example_variant2 dice-template-library::dice-template-library ) +add_executable(example_limit_allocator + example_limit_allocator.cpp) +target_link_libraries(example_limit_allocator + PRIVATE + dice-template-library::dice-template-library +) + diff --git a/examples/example_limit_allocator.cpp b/examples/example_limit_allocator.cpp new file mode 100644 index 0000000..1a22112 --- /dev/null +++ b/examples/example_limit_allocator.cpp @@ -0,0 +1,26 @@ +#include + +#include +#include + + +int main() { + std::vector> vec{dice::template_library::limit_allocator{3 * sizeof(int)}}; + vec.push_back(1); + vec.push_back(2); + + try { + vec.push_back(3); + assert(false); + } catch (...) { + } + + vec.pop_back(); + vec.push_back(4); + + try { + vec.push_back(5); + assert(false); + } catch (...) { + } +} diff --git a/include/dice/template-library/limit_allocator.hpp b/include/dice/template-library/limit_allocator.hpp new file mode 100644 index 0000000..4b25aa9 --- /dev/null +++ b/include/dice/template-library/limit_allocator.hpp @@ -0,0 +1,174 @@ +#ifndef DICE_TEMPLATELIBRARY_LIMITALLOCATOR_HPP +#define DICE_TEMPLATELIBRARY_LIMITALLOCATOR_HPP + +#include +#include +#include +#include +#include +#include + +namespace dice::template_library { + + /** + * The synchronization policy of a limit_allocator + */ + enum struct limit_allocator_syncness : bool { + sync, ///< thread-safe (synchronized) + unsync ///< not thread-safe (unsynchronized) + }; + + namespace detail_limit_allocator { + template + struct limit_allocator_control_block; + + template<> + struct limit_allocator_control_block { + std::atomic bytes_left = 0; + + void allocate(size_t n_bytes) { + auto old = bytes_left.load(std::memory_order_relaxed); + + do { + if (old < n_bytes) [[unlikely]] { + throw std::bad_alloc{}; + } + } while (!bytes_left.compare_exchange_weak(old, old - n_bytes, std::memory_order_relaxed, std::memory_order_relaxed)); + } + + void deallocate(size_t n_bytes) noexcept { + bytes_left.fetch_add(n_bytes, std::memory_order_relaxed); + } + }; + + template<> + struct limit_allocator_control_block { + size_t bytes_left = 0; + + void allocate(size_t n_bytes) { + if (bytes_left < n_bytes) [[unlikely]] { + throw std::bad_alloc{}; + } + bytes_left -= n_bytes; + } + + void deallocate(size_t n_bytes) noexcept { + bytes_left += n_bytes; + } + }; + } // namespace detail_limit_allocator + + /** + * Allocator wrapper that limits the amount of memory its underlying allocator + * is allowed to allocate. + * + * @tparam T value type of the allocator (the thing that it allocates) + * @tparam Allocator the underlying allocator + * @tparam syncness determines the synchronization of the limit + */ + template typename Allocator = std::allocator, limit_allocator_syncness syncness = limit_allocator_syncness::sync> + struct limit_allocator { + using control_block_type = detail_limit_allocator::limit_allocator_control_block; + using value_type = T; + using upstream_allocator_type = Allocator; + using pointer = typename std::allocator_traits::pointer; + using const_pointer = typename std::allocator_traits::const_pointer; + using void_pointer = typename std::allocator_traits::void_pointer; + using const_void_pointer = typename std::allocator_traits::const_void_pointer; + using size_type = typename std::allocator_traits::size_type; + using difference_type = typename std::allocator_traits::difference_type; + + 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 = limit_allocator; + }; + + private: + template typename, limit_allocator_syncness> + friend struct limit_allocator; + + std::shared_ptr control_block_; + [[no_unique_address]] upstream_allocator_type inner_; + + constexpr limit_allocator(std::shared_ptr const &control_block, upstream_allocator_type const &alloc) requires(std::is_default_constructible_v) + : control_block_{control_block}, + inner_{alloc} { + } + + public: + explicit constexpr limit_allocator(size_t bytes_limit) requires(std::is_default_constructible_v) + : control_block_{std::make_shared(bytes_limit)}, + inner_{} { + } + + constexpr limit_allocator(limit_allocator const &other) noexcept(std::is_nothrow_move_constructible_v) = default; + constexpr limit_allocator(limit_allocator &&other) noexcept(std::is_nothrow_copy_constructible_v) = default; + constexpr limit_allocator &operator=(limit_allocator const &other) noexcept(std::is_nothrow_copy_assignable_v) = default; + constexpr limit_allocator &operator=(limit_allocator &&other) noexcept(std::is_nothrow_move_assignable_v) = default; + constexpr ~limit_allocator() = default; + + template + constexpr limit_allocator(limit_allocator const &other) + noexcept(std::is_nothrow_constructible_v::upstream_allocator_type const &>) + : control_block_{other.control_block_}, + inner_{other.inner_} { + } + + constexpr limit_allocator(size_t bytes_limit, upstream_allocator_type const &upstream) + : control_block_{std::make_shared(bytes_limit)}, + inner_{upstream} { + } + + constexpr limit_allocator(size_t bytes_limit, upstream_allocator_type &&upstream) + : control_block_{std::make_shared(bytes_limit)}, + inner_{std::move(upstream)} { + } + + template + explicit constexpr limit_allocator(size_t bytes_limit, std::in_place_t, Args &&...args) + : control_block_{std::make_shared(bytes_limit)}, + inner_{std::forward(args)...} { + } + + constexpr pointer allocate(size_t n) { + control_block_->allocate(n * sizeof(T)); + + try { + return std::allocator_traits::allocate(inner_, n); + } catch (...) { + control_block_->deallocate(n * sizeof(T)); + throw; + } + } + + constexpr void deallocate(pointer ptr, size_t n) { + std::allocator_traits::deallocate(inner_, ptr, n); + control_block_->deallocate(n * sizeof(T)); + } + + constexpr limit_allocator select_on_container_copy_construction() const { + return limit_allocator{control_block_, std::allocator_traits::select_on_container_copy_construction(inner_)}; + } + + [[nodiscard]] upstream_allocator_type const &upstream_allocator() const noexcept { + return inner_; + } + + friend constexpr void swap(limit_allocator &a, limit_allocator &b) noexcept(std::is_nothrow_swappable_v) + requires(std::is_swappable_v) { + using std::swap; + swap(a.control_block_, b.control_block_); + swap(a.inner_, b.inner_); + } + + bool operator==(limit_allocator const &other) const noexcept = default; + bool operator!=(limit_allocator const &other) const noexcept = default; + }; +} // namespace dice::template_library + +#endif // DICE_TEMPLATELIBRARY_LIMITALLOCATOR_HPP \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 63181f4..2e004e7 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_limit_allocator tests_limit_allocator.cpp) +custom_add_test(tests_limit_allocator) diff --git a/tests/tests_limit_allocator.cpp b/tests/tests_limit_allocator.cpp new file mode 100644 index 0000000..7ab342c --- /dev/null +++ b/tests/tests_limit_allocator.cpp @@ -0,0 +1,39 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +#include + +TEST_SUITE("limit_allocator sanity check") { + using namespace dice::template_library; + + template + void run_tests() { + limit_allocator alloc{12 * sizeof(int)}; + + auto a = alloc.allocate(5); + auto b = alloc.allocate(5); + auto c = alloc.allocate(1); + alloc.deallocate(c, 1); + alloc.deallocate(b, 5); + + CHECK_THROWS(alloc.allocate(8)); + + auto d = alloc.allocate(7); + + alloc.deallocate(a, 5); + alloc.deallocate(d, 7); + + auto e = alloc.allocate(12); + alloc.deallocate(e, 12); + + CHECK_THROWS(alloc.allocate(13)); + } + + TEST_CASE("sync") { + run_tests(); + } + + TEST_CASE("unsync") { + run_tests(); + } +} From 748f02e42fe1ba80ebaf2797c71fd6c756e18d6d Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:42:56 +0100 Subject: [PATCH 2/4] add rebind to offset_ptr_stl_allocator --- include/dice/template-library/polymorphic_allocator.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/dice/template-library/polymorphic_allocator.hpp b/include/dice/template-library/polymorphic_allocator.hpp index 9cc3e0c..3d5afae 100644 --- a/include/dice/template-library/polymorphic_allocator.hpp +++ b/include/dice/template-library/polymorphic_allocator.hpp @@ -337,6 +337,11 @@ namespace dice::template_library { using propagate_on_container_swap = typename std::allocator_traits::propagate_on_container_swap; using is_always_equal = typename std::allocator_traits::is_always_equal; + template + struct rebind { + using other = offset_ptr_stl_allocator; + }; + private: template typename> friend struct offset_ptr_stl_allocator; From 44fcef4b1396332087a26dbdb89b49b000fc4cdd Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:56:47 +0100 Subject: [PATCH 3/4] reformat file --- .../dice/template-library/limit_allocator.hpp | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/include/dice/template-library/limit_allocator.hpp b/include/dice/template-library/limit_allocator.hpp index 4b25aa9..7bdd60b 100644 --- a/include/dice/template-library/limit_allocator.hpp +++ b/include/dice/template-library/limit_allocator.hpp @@ -15,7 +15,7 @@ namespace dice::template_library { */ enum struct limit_allocator_syncness : bool { sync, ///< thread-safe (synchronized) - unsync ///< not thread-safe (unsynchronized) + unsync///< not thread-safe (unsynchronized) }; namespace detail_limit_allocator { @@ -56,7 +56,7 @@ namespace dice::template_library { bytes_left += n_bytes; } }; - } // namespace detail_limit_allocator + }// namespace detail_limit_allocator /** * Allocator wrapper that limits the amount of memory its underlying allocator @@ -66,11 +66,11 @@ namespace dice::template_library { * @tparam Allocator the underlying allocator * @tparam syncness determines the synchronization of the limit */ - template typename Allocator = std::allocator, limit_allocator_syncness syncness = limit_allocator_syncness::sync> + template typename Allocator = std::allocator, limit_allocator_syncness syncness = limit_allocator_syncness::sync> struct limit_allocator { - using control_block_type = detail_limit_allocator::limit_allocator_control_block; + using control_block_type = detail_limit_allocator::limit_allocator_control_block; using value_type = T; - using upstream_allocator_type = Allocator; + using upstream_allocator_type = Allocator; using pointer = typename std::allocator_traits::pointer; using const_pointer = typename std::allocator_traits::const_pointer; using void_pointer = typename std::allocator_traits::void_pointer; @@ -92,18 +92,20 @@ namespace dice::template_library { template typename, limit_allocator_syncness> friend struct limit_allocator; - std::shared_ptr control_block_; + std::shared_ptr control_block_; [[no_unique_address]] upstream_allocator_type inner_; - constexpr limit_allocator(std::shared_ptr const &control_block, upstream_allocator_type const &alloc) requires(std::is_default_constructible_v) - : control_block_{control_block}, - inner_{alloc} { + constexpr limit_allocator(std::shared_ptr const &control_block, upstream_allocator_type const &alloc) + requires(std::is_default_constructible_v) + : control_block_{control_block}, + inner_{alloc} { } public: - explicit constexpr limit_allocator(size_t bytes_limit) requires(std::is_default_constructible_v) - : control_block_{std::make_shared(bytes_limit)}, - inner_{} { + explicit constexpr limit_allocator(size_t bytes_limit) + requires(std::is_default_constructible_v) + : control_block_{std::make_shared(bytes_limit)}, + inner_{} { } constexpr limit_allocator(limit_allocator const &other) noexcept(std::is_nothrow_move_constructible_v) = default; @@ -113,23 +115,22 @@ namespace dice::template_library { constexpr ~limit_allocator() = default; template - constexpr limit_allocator(limit_allocator const &other) - noexcept(std::is_nothrow_constructible_v::upstream_allocator_type const &>) + constexpr limit_allocator(limit_allocator const &other) noexcept(std::is_nothrow_constructible_v::upstream_allocator_type const &>) : control_block_{other.control_block_}, - inner_{other.inner_} { + inner_{other.inner_} { } constexpr limit_allocator(size_t bytes_limit, upstream_allocator_type const &upstream) : control_block_{std::make_shared(bytes_limit)}, - inner_{upstream} { + inner_{upstream} { } constexpr limit_allocator(size_t bytes_limit, upstream_allocator_type &&upstream) : control_block_{std::make_shared(bytes_limit)}, - inner_{std::move(upstream)} { + inner_{std::move(upstream)} { } - template + template explicit constexpr limit_allocator(size_t bytes_limit, std::in_place_t, Args &&...args) : control_block_{std::make_shared(bytes_limit)}, inner_{std::forward(args)...} { @@ -160,7 +161,8 @@ namespace dice::template_library { } friend constexpr void swap(limit_allocator &a, limit_allocator &b) noexcept(std::is_nothrow_swappable_v) - requires(std::is_swappable_v) { + requires(std::is_swappable_v) + { using std::swap; swap(a.control_block_, b.control_block_); swap(a.inner_, b.inner_); @@ -169,6 +171,6 @@ namespace dice::template_library { bool operator==(limit_allocator const &other) const noexcept = default; bool operator!=(limit_allocator const &other) const noexcept = default; }; -} // namespace dice::template_library +}// namespace dice::template_library -#endif // DICE_TEMPLATELIBRARY_LIMITALLOCATOR_HPP \ No newline at end of file +#endif// DICE_TEMPLATELIBRARY_LIMITALLOCATOR_HPP \ No newline at end of file From af74fefd29929e33704f81b645ff809b88167a37 Mon Sep 17 00:00:00 2001 From: Liss Heidrich <31625940+liss-h@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:21:01 +0100 Subject: [PATCH 4/4] fmt --- include/dice/template-library/limit_allocator.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/dice/template-library/limit_allocator.hpp b/include/dice/template-library/limit_allocator.hpp index 7bdd60b..9c5aaf7 100644 --- a/include/dice/template-library/limit_allocator.hpp +++ b/include/dice/template-library/limit_allocator.hpp @@ -15,7 +15,7 @@ namespace dice::template_library { */ enum struct limit_allocator_syncness : bool { sync, ///< thread-safe (synchronized) - unsync///< not thread-safe (unsynchronized) + unsync, ///< not thread-safe (unsynchronized) }; namespace detail_limit_allocator {