Optimize memory_resource & add monotonic_buffer_resource

This commit is contained in:
mutouyun 2025-01-10 21:24:20 +08:00
parent b8d01ddf68
commit 70a1f68f01
7 changed files with 375 additions and 58 deletions

View File

@ -15,11 +15,37 @@
#include "libipc/imp/export.h" #include "libipc/imp/export.h"
#include "libipc/imp/uninitialized.h" #include "libipc/imp/uninitialized.h"
#include "libipc/imp/byte.h" #include "libipc/imp/byte.h"
#include "libipc/mem/memory_resource.h"
namespace ipc { namespace ipc {
namespace mem { namespace mem {
/// \brief Helper trait for memory resource.
template <typename T, typename = void>
struct has_allocate : std::false_type {};
template <typename T>
struct has_allocate<T,
typename std::enable_if<std::is_convertible<
decltype(std::declval<T &>().allocate(std::declval<std::size_t>(),
std::declval<std::size_t>())), void *
>::value>::type> : std::true_type {};
template <typename T, typename = void>
struct has_deallocate : std::false_type {};
template <typename T>
struct has_deallocate<T,
decltype(std::declval<T &>().deallocate(std::declval<void *>(),
std::declval<std::size_t>(),
std::declval<std::size_t>()))
> : std::true_type {};
template <typename T>
using is_memory_resource =
std::enable_if_t<has_allocate <T>::value &&
has_deallocate<T>::value, bool>;
/** /**
* \brief An allocator which exhibits different allocation behavior * \brief An allocator which exhibits different allocation behavior
* depending upon the memory resource from which it is constructed. * depending upon the memory resource from which it is constructed.
@ -57,7 +83,7 @@ class LIBIPC_EXPORT allocator {
holder_mr(MR *p_mr) noexcept holder_mr(MR *p_mr) noexcept
: res_(p_mr) {} : res_(p_mr) {}
// [MSVC] error C2259: 'pmr::allocator::holder_mr<void *,bool>': cannot instantiate abstract class. // [MSVC] error C2259: 'allocator::holder_mr<void *,bool>': cannot instantiate abstract class.
void *alloc(std::size_t s, std::size_t a) const override { return nullptr; } 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 {} 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 * \tparam MR memory resource type
*/ */
template <typename MR> template <typename MR>
class holder_mr<MR, verify_memory_resource<MR>> : public holder_mr<MR, void> { class holder_mr<MR, is_memory_resource<MR>> : public holder_mr<MR, void> {
using base_t = holder_mr<MR, void>; using base_t = holder_mr<MR, void>;
public: public:
@ -89,6 +115,8 @@ class LIBIPC_EXPORT allocator {
holder_mr_base & get_holder() noexcept; holder_mr_base & get_holder() noexcept;
holder_mr_base const &get_holder() const noexcept; holder_mr_base const &get_holder() const noexcept;
void init_default_resource() noexcept;
public: public:
/// \brief Constructs an `allocator` using the return value of /// \brief Constructs an `allocator` using the return value of
/// `new_delete_resource::get()` as the underlying memory resource. /// `new_delete_resource::get()` as the underlying memory resource.
@ -103,13 +131,13 @@ public:
/// \brief Constructs a allocator from a memory resource pointer. /// \brief Constructs a allocator from a memory resource pointer.
/// \note The lifetime of the pointer must be longer than that of allocator. /// \note The lifetime of the pointer must be longer than that of allocator.
template <typename T, verify_memory_resource<T> = true> template <typename T, is_memory_resource<T> = true>
allocator(T *p_mr) noexcept { allocator(T *p_mr) noexcept {
if (p_mr == nullptr) { if (p_mr == nullptr) {
ipc::construct<holder_mr<new_delete_resource>>(holder_.data(), new_delete_resource::get()); init_default_resource();
return; return;
} }
ipc::construct<holder_mr<T>>(holder_.data(), p_mr); std::ignore = ipc::construct<holder_mr<T>>(holder_.data(), p_mr);
} }
void swap(allocator &other) noexcept; void swap(allocator &other) noexcept;

View File

@ -1,7 +1,7 @@
/** /**
* \file libipc/memory_resource.h * \file libipc/memory_resource.h
* \author mutouyun (orz@orzz.org) * \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 #pragma once
@ -9,38 +9,13 @@
#include <cstddef> // std::size_t, std::max_align_t #include <cstddef> // std::size_t, std::max_align_t
#include "libipc/imp/export.h" #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 ipc {
namespace mem { namespace mem {
/// \brief Helper trait for memory resource.
template <typename T, typename = void>
struct has_allocate : std::false_type {};
template <typename T>
struct has_allocate<T,
typename std::enable_if<std::is_convertible<
decltype(std::declval<T &>().allocate(std::declval<std::size_t>(),
std::declval<std::size_t>())), void *
>::value>::type> : std::true_type {};
template <typename T, typename = void>
struct has_deallocate : std::false_type {};
template <typename T>
struct has_deallocate<T,
decltype(std::declval<T &>().deallocate(std::declval<void *>(),
std::declval<std::size_t>(),
std::declval<std::size_t>()))
> : std::true_type {};
template <typename T>
using verify_memory_resource =
std::enable_if_t<has_allocate <T>::value &&
has_deallocate<T>::value, bool>;
/** /**
* \class LIBIPC_EXPORT new_delete_resource * \class LIBIPC_EXPORT new_delete_resource
* \brief A memory resource that uses the * \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; 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<ipc::byte> buffer) noexcept;
monotonic_buffer_resource(ipc::span<ipc::byte> 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 mem
} // namespace ipc } // namespace ipc

View File

@ -3,6 +3,7 @@
#include "libipc/imp/log.h" #include "libipc/imp/log.h"
#include "libipc/mem/allocator.h" #include "libipc/mem/allocator.h"
#include "libipc/mem/memory_resource.h"
namespace ipc { namespace ipc {
namespace mem { namespace mem {
@ -15,6 +16,10 @@ allocator::holder_mr_base const &allocator::get_holder() const noexcept {
return *reinterpret_cast<holder_mr_base const *>(holder_.data()); return *reinterpret_cast<holder_mr_base const *>(holder_.data());
} }
void allocator::init_default_resource() noexcept {
std::ignore = ipc::construct<holder_mr<new_delete_resource>>(holder_.data(), new_delete_resource::get());
}
allocator::allocator() noexcept allocator::allocator() noexcept
: allocator(new_delete_resource::get()) {} : allocator(new_delete_resource::get()) {}

View File

@ -0,0 +1,140 @@
#include <utility>
#include <memory>
#include <algorithm>
#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 <typename Node>
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<Node *>(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<ipc::byte> buffer) noexcept
: monotonic_buffer_resource(buffer, allocator{}) {}
monotonic_buffer_resource::monotonic_buffer_resource(ipc::span<ipc::byte> 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<std::size_t>(tail_ - head_);
if (std::align(alignment, bytes, p, s) == nullptr) {
next_size_ = (std::max)(next_size_, bytes);
auto *node = make_node<monotonic_buffer_resource::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<ipc::byte *>(p) + s;
}
head_ = static_cast<ipc::byte *>(p) + bytes;
return p;
}
void monotonic_buffer_resource::deallocate(void *p, std::size_t bytes, std::size_t alignment) noexcept {
static_cast<void>(p);
static_cast<void>(bytes);
static_cast<void>(alignment);
// Do nothing.
}
} // namespace mem
} // namespace ipc

View File

@ -4,7 +4,12 @@
#include "test.h" #include "test.h"
#if defined(LIBIPC_CPP_17) && defined(__cpp_lib_memory_resource)
# include <memory_resource>
#endif
#include "libipc/mem/allocator.h" #include "libipc/mem/allocator.h"
#include "libipc/mem/memory_resource.h"
TEST(allocator, construct) { TEST(allocator, construct) {
ipc::mem::allocator alc; ipc::mem::allocator alc;
@ -32,6 +37,26 @@ public:
} // namespace } // namespace
TEST(allocator, memory_resource_traits) {
EXPECT_FALSE(ipc::mem::has_allocate<void>::value);
EXPECT_FALSE(ipc::mem::has_allocate<int>::value);
EXPECT_FALSE(ipc::mem::has_allocate<std::vector<int>>::value);
EXPECT_FALSE(ipc::mem::has_allocate<std::allocator<int>>::value);
#if defined(LIBIMP_CPP_17) && defined(__cpp_lib_memory_resource)
EXPECT_TRUE (ipc::mem::has_allocate<std::ipc::mem::memory_resource>::value);
EXPECT_TRUE (ipc::mem::has_allocate<std::ipc::mem::polymorphic_allocator<int>>::value);
#endif
EXPECT_FALSE(ipc::mem::has_deallocate<void>::value);
EXPECT_FALSE(ipc::mem::has_deallocate<int>::value);
EXPECT_FALSE(ipc::mem::has_deallocate<std::vector<int>>::value);
EXPECT_FALSE(ipc::mem::has_deallocate<std::allocator<int>>::value);
#if defined(LIBIMP_CPP_17) && defined(__cpp_lib_memory_resource)
EXPECT_TRUE (ipc::mem::has_deallocate<std::ipc::mem::memory_resource>::value);
EXPECT_FALSE(ipc::mem::has_deallocate<std::ipc::mem::polymorphic_allocator<int>>::value);
#endif
}
TEST(allocator, construct_copy_move) { TEST(allocator, construct_copy_move) {
ipc::mem::new_delete_resource mem_res; ipc::mem::new_delete_resource mem_res;
dummy_resource dummy_res; dummy_resource dummy_res;

View File

@ -2,9 +2,6 @@
#include "test.h" #include "test.h"
#include <utility> #include <utility>
#if defined(LIBIPC_CPP_17) && defined(__cpp_lib_memory_resource)
# include <memory_resource>
#endif
#include "libipc/mem/memory_resource.h" #include "libipc/mem/memory_resource.h"
@ -24,26 +21,6 @@ void *test_mr(T &&mr, std::size_t bytes, std::size_t alignment) {
} // namespace } // namespace
TEST(memory_resource, traits) {
EXPECT_FALSE(ipc::mem::has_allocate<void>::value);
EXPECT_FALSE(ipc::mem::has_allocate<int>::value);
EXPECT_FALSE(ipc::mem::has_allocate<std::vector<int>>::value);
EXPECT_FALSE(ipc::mem::has_allocate<std::allocator<int>>::value);
#if defined(LIBIMP_CPP_17) && defined(__cpp_lib_memory_resource)
EXPECT_TRUE (ipc::mem::has_allocate<std::ipc::mem::memory_resource>::value);
EXPECT_TRUE (ipc::mem::has_allocate<std::ipc::mem::polymorphic_allocator<int>>::value);
#endif
EXPECT_FALSE(ipc::mem::has_deallocate<void>::value);
EXPECT_FALSE(ipc::mem::has_deallocate<int>::value);
EXPECT_FALSE(ipc::mem::has_deallocate<std::vector<int>>::value);
EXPECT_FALSE(ipc::mem::has_deallocate<std::allocator<int>>::value);
#if defined(LIBIMP_CPP_17) && defined(__cpp_lib_memory_resource)
EXPECT_TRUE (ipc::mem::has_deallocate<std::ipc::mem::memory_resource>::value);
EXPECT_FALSE(ipc::mem::has_deallocate<std::ipc::mem::polymorphic_allocator<int>>::value);
#endif
}
TEST(memory_resource, new_delete_resource) { TEST(memory_resource, new_delete_resource) {
ipc::mem::new_delete_resource mem_res; 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, 8), nullptr);
EXPECT_NE(test_mr(mem_res, 1, 64), 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::byte>{}};
ipc::mem::monotonic_buffer_resource{ipc::span<ipc::byte>{}, ipc::mem::allocator{}};
SUCCEED();
}
TEST(memory_resource, monotonic_buffer_resource_no_copy) {
EXPECT_FALSE(std::is_copy_constructible<ipc::mem::monotonic_buffer_resource>::value);
EXPECT_FALSE(std::is_copy_assignable<ipc::mem::monotonic_buffer_resource>::value);
EXPECT_FALSE(std::is_move_constructible<ipc::mem::monotonic_buffer_resource>::value);
EXPECT_FALSE(std::is_move_assignable<ipc::mem::monotonic_buffer_resource>::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<ipc::byte, 4096> 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<ipc::byte, 4096> 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);
}