From e7a8005f5886140f019bd0842d5ce0d89cff4e1f Mon Sep 17 00:00:00 2001 From: mutouyun Date: Fri, 10 Jan 2025 21:24:20 +0800 Subject: [PATCH] Optimize `memory_resource` & add `monotonic_buffer_resource` --- include/libipc/mem/allocator.h | 40 ++++- include/libipc/mem/memory_resource.h | 75 +++++---- src/libipc/mem/allocator.cpp | 5 + src/libipc/mem/monotonic_buffer_resource.cpp | 140 +++++++++++++++++ ...y_resource.cpp => new_delete_resource.cpp} | 0 test/mem/test_mem_allocator.cpp | 25 +++ test/mem/test_mem_memory_resource.cpp | 148 +++++++++++++++--- 7 files changed, 375 insertions(+), 58 deletions(-) create mode 100644 src/libipc/mem/monotonic_buffer_resource.cpp rename src/libipc/mem/{memory_resource.cpp => new_delete_resource.cpp} (100%) diff --git a/include/libipc/mem/allocator.h b/include/libipc/mem/allocator.h index 17ea01d..092737c 100644 --- a/include/libipc/mem/allocator.h +++ b/include/libipc/mem/allocator.h @@ -15,11 +15,37 @@ #include "libipc/imp/export.h" #include "libipc/imp/uninitialized.h" #include "libipc/imp/byte.h" -#include "libipc/mem/memory_resource.h" namespace ipc { namespace mem { +/// \brief Helper trait for memory resource. + +template +struct has_allocate : std::false_type {}; + +template +struct has_allocate().allocate(std::declval(), + std::declval())), void * + >::value>::type> : std::true_type {}; + +template +struct has_deallocate : std::false_type {}; + +template +struct has_deallocate().deallocate(std::declval(), + std::declval(), + std::declval())) + > : std::true_type {}; + +template +using is_memory_resource = + std::enable_if_t::value && + has_deallocate::value, bool>; + /** * \brief An allocator which exhibits different allocation behavior * depending upon the memory resource from which it is constructed. @@ -57,7 +83,7 @@ class LIBIPC_EXPORT allocator { holder_mr(MR *p_mr) noexcept : res_(p_mr) {} - // [MSVC] error C2259: 'pmr::allocator::holder_mr': cannot instantiate abstract class. + // [MSVC] error C2259: 'allocator::holder_mr': cannot instantiate abstract class. void *alloc(std::size_t s, std::size_t a) const override { return nullptr; } void dealloc(void *p, std::size_t s, std::size_t a) const override {} }; @@ -67,7 +93,7 @@ class LIBIPC_EXPORT allocator { * \tparam MR memory resource type */ template - class holder_mr> : public holder_mr { + class holder_mr> : public holder_mr { using base_t = holder_mr; public: @@ -89,6 +115,8 @@ class LIBIPC_EXPORT allocator { holder_mr_base & get_holder() noexcept; holder_mr_base const &get_holder() const noexcept; + void init_default_resource() noexcept; + public: /// \brief Constructs an `allocator` using the return value of /// `new_delete_resource::get()` as the underlying memory resource. @@ -103,13 +131,13 @@ public: /// \brief Constructs a allocator from a memory resource pointer. /// \note The lifetime of the pointer must be longer than that of allocator. - template = true> + template = true> allocator(T *p_mr) noexcept { if (p_mr == nullptr) { - ipc::construct>(holder_.data(), new_delete_resource::get()); + init_default_resource(); return; } - ipc::construct>(holder_.data(), p_mr); + std::ignore = ipc::construct>(holder_.data(), p_mr); } void swap(allocator &other) noexcept; diff --git a/include/libipc/mem/memory_resource.h b/include/libipc/mem/memory_resource.h index 4a26223..47cc90b 100644 --- a/include/libipc/mem/memory_resource.h +++ b/include/libipc/mem/memory_resource.h @@ -1,7 +1,7 @@ /** * \file libipc/memory_resource.h * \author mutouyun (orz@orzz.org) - * \brief Implement memory allocation strategies that can be used by pmr::allocator. + * \brief Implement memory allocation strategies that can be used by ipc::mem::allocator. */ #pragma once @@ -9,38 +9,13 @@ #include // std::size_t, std::max_align_t #include "libipc/imp/export.h" -#include "libipc/def.h" +#include "libipc/imp/span.h" +#include "libipc/imp/byte.h" +#include "libipc/mem/allocator.h" namespace ipc { namespace mem { -/// \brief Helper trait for memory resource. - -template -struct has_allocate : std::false_type {}; - -template -struct has_allocate().allocate(std::declval(), - std::declval())), void * - >::value>::type> : std::true_type {}; - -template -struct has_deallocate : std::false_type {}; - -template -struct has_deallocate().deallocate(std::declval(), - std::declval(), - std::declval())) - > : std::true_type {}; - -template -using verify_memory_resource = - std::enable_if_t::value && - has_deallocate::value, bool>; - /** * \class LIBIPC_EXPORT new_delete_resource * \brief A memory resource that uses the @@ -62,5 +37,47 @@ public: void deallocate(void *p, std::size_t bytes, std::size_t alignment = alignof(std::max_align_t)) noexcept; }; +/** + * \class LIBIPC_EXPORT monotonic_buffer_resource + * \brief A special-purpose memory resource class + * that releases the allocated memory only when the resource is destroyed. + * \see https://en.cppreference.com/w/cpp/memory/monotonic_buffer_resource + */ +class LIBIPC_EXPORT monotonic_buffer_resource { + + allocator upstream_; + + struct node { + node *next; + std::size_t size; + } *free_list_; + + ipc::byte * head_; + ipc::byte * tail_; + std::size_t next_size_; + + ipc::byte * const initial_buffer_; + std::size_t const initial_size_; + +public: + monotonic_buffer_resource() noexcept; + explicit monotonic_buffer_resource(allocator upstream) noexcept; + explicit monotonic_buffer_resource(std::size_t initial_size) noexcept; + monotonic_buffer_resource(std::size_t initial_size, allocator upstream) noexcept; + monotonic_buffer_resource(ipc::span buffer) noexcept; + monotonic_buffer_resource(ipc::span buffer, allocator upstream) noexcept; + + ~monotonic_buffer_resource() noexcept; + + monotonic_buffer_resource(monotonic_buffer_resource const &) = delete; + monotonic_buffer_resource &operator=(monotonic_buffer_resource const &) = delete; + + allocator upstream_resource() const noexcept; + void release() noexcept; + + void *allocate(std::size_t bytes, std::size_t alignment = alignof(std::max_align_t)) noexcept; + void deallocate(void *p, std::size_t bytes, std::size_t alignment = alignof(std::max_align_t)) noexcept; +}; + } // namespace mem } // namespace ipc diff --git a/src/libipc/mem/allocator.cpp b/src/libipc/mem/allocator.cpp index 187d22b..566b17e 100644 --- a/src/libipc/mem/allocator.cpp +++ b/src/libipc/mem/allocator.cpp @@ -3,6 +3,7 @@ #include "libipc/imp/log.h" #include "libipc/mem/allocator.h" +#include "libipc/mem/memory_resource.h" namespace ipc { namespace mem { @@ -15,6 +16,10 @@ allocator::holder_mr_base const &allocator::get_holder() const noexcept { return *reinterpret_cast(holder_.data()); } +void allocator::init_default_resource() noexcept { + std::ignore = ipc::construct>(holder_.data(), new_delete_resource::get()); +} + allocator::allocator() noexcept : allocator(new_delete_resource::get()) {} diff --git a/src/libipc/mem/monotonic_buffer_resource.cpp b/src/libipc/mem/monotonic_buffer_resource.cpp new file mode 100644 index 0000000..22478e6 --- /dev/null +++ b/src/libipc/mem/monotonic_buffer_resource.cpp @@ -0,0 +1,140 @@ + +#include +#include +#include + +#include "libipc/imp/log.h" +#include "libipc/imp/aligned.h" +#include "libipc/imp/detect_plat.h" +#include "libipc/mem/memory_resource.h" + +namespace ipc { +namespace mem { +namespace { + +template +Node *make_node(allocator const &upstream, std::size_t initial_size, std::size_t alignment) noexcept { + LIBIPC_LOG(); + auto sz = ipc::round_up(sizeof(Node), alignment) + initial_size; + LIBIPC_TRY { + auto *node = static_cast(upstream.allocate(sz)); + if (node == nullptr) { + log.error("failed: allocate memory for `monotonic_buffer_resource`'s node.", + " bytes = ", initial_size, ", alignment = ", alignment); + return nullptr; + } + node->next = nullptr; + node->size = sz; + return node; + } LIBIPC_CATCH(...) { + log.error("failed: allocate memory for `monotonic_buffer_resource`'s node.", + " bytes = ", initial_size, ", alignment = ", alignment, + "\n\texception: ", ipc::log::exception_string(std::current_exception())); + return nullptr; + } +} + +std::size_t next_buffer_size(std::size_t size) noexcept { + return size * 3 / 2; +} + +} // namespace + +monotonic_buffer_resource::monotonic_buffer_resource() noexcept + : monotonic_buffer_resource(allocator{}) {} + +monotonic_buffer_resource::monotonic_buffer_resource(allocator upstream) noexcept + : monotonic_buffer_resource(0, std::move(upstream)) {} + +monotonic_buffer_resource::monotonic_buffer_resource(std::size_t initial_size) noexcept + : monotonic_buffer_resource(initial_size, allocator{}) {} + +monotonic_buffer_resource::monotonic_buffer_resource(std::size_t initial_size, allocator upstream) noexcept + : upstream_ (std::move(upstream)) + , free_list_ (nullptr) + , head_ (nullptr) + , tail_ (nullptr) + , next_size_ (initial_size) + , initial_buffer_(nullptr) + , initial_size_ (initial_size) {} + +monotonic_buffer_resource::monotonic_buffer_resource(ipc::span buffer) noexcept + : monotonic_buffer_resource(buffer, allocator{}) {} + +monotonic_buffer_resource::monotonic_buffer_resource(ipc::span buffer, allocator upstream) noexcept + : upstream_ (std::move(upstream)) + , free_list_ (nullptr) + , head_ (buffer.begin()) + , tail_ (buffer.end()) + , next_size_ (next_buffer_size(buffer.size())) + , initial_buffer_(buffer.begin()) + , initial_size_ (buffer.size()) {} + +monotonic_buffer_resource::~monotonic_buffer_resource() noexcept { + release(); +} + +allocator monotonic_buffer_resource::upstream_resource() const noexcept { + return upstream_; +} + +void monotonic_buffer_resource::release() noexcept { + LIBIPC_LOG(); + LIBIPC_TRY { + while (free_list_ != nullptr) { + auto *next = free_list_->next; + upstream_.deallocate(free_list_, free_list_->size); + free_list_ = next; + } + } LIBIPC_CATCH(...) { + log.error("failed: deallocate memory for `monotonic_buffer_resource`.", + "\n\texception: ", ipc::log::exception_string(std::current_exception())); + } + // reset to initial state at contruction + if ((head_ = initial_buffer_) != nullptr) { + tail_ = head_ + initial_size_; + next_size_ = next_buffer_size(initial_size_); + } else { + tail_ = nullptr; + next_size_ = initial_size_; + } +} + +void *monotonic_buffer_resource::allocate(std::size_t bytes, std::size_t alignment) noexcept { + LIBIPC_LOG(); + if (bytes == 0) { + log.error("failed: allocate bytes = 0."); + return nullptr; + } + void *p = head_; + auto s = static_cast(tail_ - head_); + if (std::align(alignment, bytes, p, s) == nullptr) { + next_size_ = (std::max)(next_size_, bytes); + auto *node = make_node(upstream_, next_size_, alignment); + if (node == nullptr) return nullptr; + node->next = free_list_; + free_list_ = node; + next_size_ = next_buffer_size(next_size_); + // try again + s = node->size - sizeof(monotonic_buffer_resource::node); + p = std::align(alignment, bytes, (p = node + 1), s); + if (p == nullptr) { + log.error("failed: allocate memory for `monotonic_buffer_resource`.", + " bytes = ", bytes, ", alignment = ", alignment); + return nullptr; + } + tail_ = static_cast(p) + s; + } + head_ = static_cast(p) + bytes; + return p; +} + +void monotonic_buffer_resource::deallocate(void *p, std::size_t bytes, std::size_t alignment) noexcept { + static_cast(p); + static_cast(bytes); + static_cast(alignment); + // Do nothing. +} + +} // namespace mem +} // namespace ipc diff --git a/src/libipc/mem/memory_resource.cpp b/src/libipc/mem/new_delete_resource.cpp similarity index 100% rename from src/libipc/mem/memory_resource.cpp rename to src/libipc/mem/new_delete_resource.cpp diff --git a/test/mem/test_mem_allocator.cpp b/test/mem/test_mem_allocator.cpp index 389b959..fcfcba0 100644 --- a/test/mem/test_mem_allocator.cpp +++ b/test/mem/test_mem_allocator.cpp @@ -4,7 +4,12 @@ #include "test.h" +#if defined(LIBIPC_CPP_17) && defined(__cpp_lib_memory_resource) +# include +#endif + #include "libipc/mem/allocator.h" +#include "libipc/mem/memory_resource.h" TEST(allocator, construct) { ipc::mem::allocator alc; @@ -32,6 +37,26 @@ public: } // namespace +TEST(allocator, memory_resource_traits) { + EXPECT_FALSE(ipc::mem::has_allocate::value); + EXPECT_FALSE(ipc::mem::has_allocate::value); + EXPECT_FALSE(ipc::mem::has_allocate>::value); + EXPECT_FALSE(ipc::mem::has_allocate>::value); +#if defined(LIBIMP_CPP_17) && defined(__cpp_lib_memory_resource) + EXPECT_TRUE (ipc::mem::has_allocate::value); + EXPECT_TRUE (ipc::mem::has_allocate>::value); +#endif + + EXPECT_FALSE(ipc::mem::has_deallocate::value); + EXPECT_FALSE(ipc::mem::has_deallocate::value); + EXPECT_FALSE(ipc::mem::has_deallocate>::value); + EXPECT_FALSE(ipc::mem::has_deallocate>::value); +#if defined(LIBIMP_CPP_17) && defined(__cpp_lib_memory_resource) + EXPECT_TRUE (ipc::mem::has_deallocate::value); + EXPECT_FALSE(ipc::mem::has_deallocate>::value); +#endif +} + TEST(allocator, construct_copy_move) { ipc::mem::new_delete_resource mem_res; dummy_resource dummy_res; diff --git a/test/mem/test_mem_memory_resource.cpp b/test/mem/test_mem_memory_resource.cpp index a7a7ae4..fb4ac86 100644 --- a/test/mem/test_mem_memory_resource.cpp +++ b/test/mem/test_mem_memory_resource.cpp @@ -2,9 +2,6 @@ #include "test.h" #include -#if defined(LIBIPC_CPP_17) && defined(__cpp_lib_memory_resource) -# include -#endif #include "libipc/mem/memory_resource.h" @@ -24,26 +21,6 @@ void *test_mr(T &&mr, std::size_t bytes, std::size_t alignment) { } // namespace -TEST(memory_resource, traits) { - EXPECT_FALSE(ipc::mem::has_allocate::value); - EXPECT_FALSE(ipc::mem::has_allocate::value); - EXPECT_FALSE(ipc::mem::has_allocate>::value); - EXPECT_FALSE(ipc::mem::has_allocate>::value); -#if defined(LIBIMP_CPP_17) && defined(__cpp_lib_memory_resource) - EXPECT_TRUE (ipc::mem::has_allocate::value); - EXPECT_TRUE (ipc::mem::has_allocate>::value); -#endif - - EXPECT_FALSE(ipc::mem::has_deallocate::value); - EXPECT_FALSE(ipc::mem::has_deallocate::value); - EXPECT_FALSE(ipc::mem::has_deallocate>::value); - EXPECT_FALSE(ipc::mem::has_deallocate>::value); -#if defined(LIBIMP_CPP_17) && defined(__cpp_lib_memory_resource) - EXPECT_TRUE (ipc::mem::has_deallocate::value); - EXPECT_FALSE(ipc::mem::has_deallocate>::value); -#endif -} - TEST(memory_resource, new_delete_resource) { ipc::mem::new_delete_resource mem_res; @@ -61,3 +38,128 @@ TEST(memory_resource, new_delete_resource) { EXPECT_NE(test_mr(mem_res, 1, 8), nullptr); EXPECT_NE(test_mr(mem_res, 1, 64), nullptr); } + +TEST(memory_resource, monotonic_buffer_resource_construct) { + { ipc::mem::monotonic_buffer_resource tmp; } + ipc::mem::monotonic_buffer_resource{}; + ipc::mem::monotonic_buffer_resource{ipc::mem::allocator{}}; + ipc::mem::monotonic_buffer_resource{0}; + ipc::mem::monotonic_buffer_resource{0, ipc::mem::allocator{}}; + ipc::mem::monotonic_buffer_resource{ipc::span{}}; + ipc::mem::monotonic_buffer_resource{ipc::span{}, ipc::mem::allocator{}}; + SUCCEED(); +} + +TEST(memory_resource, monotonic_buffer_resource_no_copy) { + EXPECT_FALSE(std::is_copy_constructible::value); + EXPECT_FALSE(std::is_copy_assignable::value); + EXPECT_FALSE(std::is_move_constructible::value); + EXPECT_FALSE(std::is_move_assignable::value); +} + +TEST(memory_resource, monotonic_buffer_resource_upstream_resource) { + struct dummy_allocator { + bool allocated = false; + void *allocate(std::size_t, std::size_t) noexcept { allocated = true; return nullptr; } + void deallocate(void *, std::size_t, std::size_t) noexcept {} + } dummy; + ipc::mem::monotonic_buffer_resource tmp{&dummy}; + ASSERT_EQ(tmp.upstream_resource().allocate(1), nullptr); + ASSERT_TRUE(dummy.allocated); +} + +namespace { + +struct dummy_allocator { + std::size_t allocated = 0; + void *allocate(std::size_t size, std::size_t) noexcept { + allocated += size; + return std::malloc(size); + } + void deallocate(void *p, std::size_t size, std::size_t) noexcept { + allocated -= size; + std::free(p); + } +}; + +} // namespace + +TEST(memory_resource, monotonic_buffer_resource_allocate) { + dummy_allocator dummy; + { + ipc::mem::monotonic_buffer_resource tmp{&dummy}; + ASSERT_EQ(tmp.allocate(0), nullptr); + ASSERT_EQ(dummy.allocated, 0); + } + ASSERT_EQ(dummy.allocated, 0); + { + ipc::mem::monotonic_buffer_resource tmp{&dummy}; + std::size_t sz = 0; + for (std::size_t i = 1; i < 1024; ++i) { + ASSERT_NE(tmp.allocate(i), nullptr); + sz += i; + } + for (std::size_t i = 1; i < 1024; ++i) { + ASSERT_NE(tmp.allocate(1024 - i), nullptr); + sz += 1024 - i; + } + ASSERT_GE(dummy.allocated, sz); + } + ASSERT_EQ(dummy.allocated, 0); +} + +TEST(memory_resource, monotonic_buffer_resource_allocate_by_buffer) { + dummy_allocator dummy; + std::array buffer; + { + ipc::mem::monotonic_buffer_resource tmp{buffer, &dummy}; + for (std::size_t i = 1; i < 64; ++i) { + ASSERT_NE(tmp.allocate(i), nullptr); + } + ASSERT_EQ(dummy.allocated, 0); + std::size_t sz = 0; + for (std::size_t i = 1; i < 64; ++i) { + ASSERT_NE(tmp.allocate(64 - i), nullptr); + sz += 64 - i; + } + ASSERT_GT(dummy.allocated, sz); + } + ASSERT_EQ(dummy.allocated, 0); +} + +TEST(memory_resource, monotonic_buffer_resource_release) { + dummy_allocator dummy; + { + ipc::mem::monotonic_buffer_resource tmp{&dummy}; + tmp.release(); + ASSERT_EQ(dummy.allocated, 0); + ASSERT_NE(tmp.allocate(1024), nullptr); + ASSERT_GE(dummy.allocated, 1024u); + ASSERT_LE(dummy.allocated, 1024u * 1.5); + tmp.release(); + ASSERT_EQ(dummy.allocated, 0); + ASSERT_NE(tmp.allocate(1024), nullptr); + ASSERT_GE(dummy.allocated, 1024u); + ASSERT_LE(dummy.allocated, 1024u * 1.5); + } + ASSERT_EQ(dummy.allocated, 0); + std::array buffer; + { + ipc::mem::monotonic_buffer_resource tmp{buffer, &dummy}; + auto *p = tmp.allocate(1024); + ASSERT_EQ(p, buffer.data()); + ASSERT_EQ(dummy.allocated, 0); + p = tmp.allocate(10240); + ASSERT_NE(p, buffer.data()); + ASSERT_LE(dummy.allocated, 10240u + 1024u); + tmp.release(); + ASSERT_EQ(dummy.allocated, 0); + p = tmp.allocate(1024); + ASSERT_EQ(p, buffer.data()); + ASSERT_EQ(dummy.allocated, 0); + p = tmp.allocate(10240); + ASSERT_NE(p, buffer.data()); + ASSERT_LE(dummy.allocated, 10240u + 1024u); + } + ASSERT_EQ(dummy.allocated, 0); +}