From edc1e80585fcfd132b2f916d9d11aa4239faf5cd Mon Sep 17 00:00:00 2001 From: mutouyun Date: Tue, 21 Jan 2025 17:37:53 +0800 Subject: [PATCH] Add `block_pool` --- include/libipc/mem/block_pool.h | 121 ++++++++++++++++ include/libipc/mem/central_cache_allocator.h | 20 +++ include/libipc/mem/central_cache_pool.h | 132 ++++++++++++++++++ src/libipc/mem/central_cache_allocator.cpp | 47 +++++++ test/mem/test_mem_block_pool.cpp | 32 +++++ test/mem/test_mem_central_cache_allocator.cpp | 15 ++ test/mem/test_mem_central_cache_pool.cpp | 44 ++++++ 7 files changed, 411 insertions(+) create mode 100644 include/libipc/mem/block_pool.h create mode 100644 include/libipc/mem/central_cache_allocator.h create mode 100644 include/libipc/mem/central_cache_pool.h create mode 100644 src/libipc/mem/central_cache_allocator.cpp create mode 100644 test/mem/test_mem_block_pool.cpp create mode 100644 test/mem/test_mem_central_cache_allocator.cpp create mode 100644 test/mem/test_mem_central_cache_pool.cpp diff --git a/include/libipc/mem/block_pool.h b/include/libipc/mem/block_pool.h new file mode 100644 index 0000000..9906a61 --- /dev/null +++ b/include/libipc/mem/block_pool.h @@ -0,0 +1,121 @@ +/** + * \file libipc/block_pool.h + * \author mutouyun (orz@orzz.org) + * \brief The fixed-length memory block pool. + */ +#pragma once + +#include + +#include "libipc/mem/central_cache_pool.h" + +namespace ipc { +namespace mem { + +/** + * \brief Fixed-length memory block pool. + * \tparam BlockSize specifies the memory block size + * \tparam BlockPoolExpansion specifies the default number of blocks to expand when the block pool is exhausted + */ +template +class block_pool; + +/// \brief General-purpose block pool for any size of memory block. +/// \note This block pool can only be used to deallocate a group of memory blocks of unknown but consistent size, +/// and cannot be used for memory block allocation. +template <> +class block_pool<0, 0> { + + template + friend class block_pool; + + /// \brief The block type. + struct block_t { + block_t *next; + }; + + /// \brief The central cache pool type. + using central_cache_pool_t = central_cache_pool; + +public: + static constexpr std::size_t block_size = 0; + + block_pool() noexcept : cursor_(central_cache_pool_t::instance().aqueire()) {} + ~block_pool() noexcept { + central_cache_pool_t::instance().release(cursor_); + } + + block_pool(block_pool const &) = delete; + block_pool& operator=(block_pool const &) = delete; + + block_pool(block_pool &&rhs) noexcept : cursor_(std::exchange(rhs.cursor_, nullptr)) {} + block_pool &operator=(block_pool &&) noexcept = delete; + + void deallocate(void *p) noexcept { + if (p == nullptr) return; + block_t *b = static_cast(p); + b->next = cursor_; + cursor_ = b; + } + +private: + block_t *cursor_; +}; + +/// \brief A block pool for a block of memory of a specific size. +template +class block_pool { + + /// \brief The block type. + using block_t = block; + /// \brief The central cache pool type. + using central_cache_pool_t = central_cache_pool; + + /// \brief Expand the block pool when it is exhausted. + block_t *expand() noexcept { + return central_cache_pool_t::instance().aqueire(); + } + +public: + static constexpr std::size_t block_size = BlockSize; + + block_pool() noexcept : cursor_(expand()) {} + ~block_pool() noexcept { + central_cache_pool_t::instance().release(cursor_); + } + + block_pool(block_pool const &) = delete; + block_pool& operator=(block_pool const &) = delete; + + block_pool(block_pool &&rhs) noexcept + : cursor_(std::exchange(rhs.cursor_, nullptr)) {} + block_pool &operator=(block_pool &&) noexcept = delete; + + /// \brief Used to take all memory blocks from within a general-purpose block pool. + /// \note Of course, the actual memory blocks they manage must be the same size. + block_pool(block_pool<0, 0> &&rhs) noexcept + : cursor_(reinterpret_cast(std::exchange(rhs.cursor_, nullptr))) {} + + void *allocate() noexcept { + if (cursor_ == nullptr) { + cursor_ = expand(); + if (cursor_ == nullptr) return nullptr; + } + block_t *p = cursor_; + cursor_ = cursor_->next; + return p->storage.data(); + } + + void deallocate(void *p) noexcept { + if (p == nullptr) return; + block_t *b = static_cast(p); + b->next = cursor_; + cursor_ = b; + } + +private: + block_t *cursor_; +}; + +} // namespace mem +} // namespace ipc diff --git a/include/libipc/mem/central_cache_allocator.h b/include/libipc/mem/central_cache_allocator.h new file mode 100644 index 0000000..d4770b6 --- /dev/null +++ b/include/libipc/mem/central_cache_allocator.h @@ -0,0 +1,20 @@ +/** + * \file libipc/central_cache_allocator.h + * \author mutouyun (orz@orzz.org) + * \brief The central cache allocator getter. + */ +#pragma once + +#include "libipc/imp/export.h" +#include "libipc/mem/polymorphic_allocator.h" + +namespace ipc { +namespace mem { + +/// \brief Get the central cache allocator. +/// \note The central cache allocator is used to allocate memory for the central cache pool. +/// The underlying memory resource is a `monotonic_buffer_resource` with a fixed-size buffer. +LIBIPC_EXPORT bytes_allocator ¢ral_cache_allocator() noexcept; + +} // namespace mem +} // namespace ipc diff --git a/include/libipc/mem/central_cache_pool.h b/include/libipc/mem/central_cache_pool.h new file mode 100644 index 0000000..e6a3bd0 --- /dev/null +++ b/include/libipc/mem/central_cache_pool.h @@ -0,0 +1,132 @@ +/** + * \file libipc/central_cache_pool.h + * \author mutouyun (orz@orzz.org) + * \brief The fixed-length memory block central cache pool. + */ +#pragma once + +#include +#include +#include +#include + +#include "libipc/imp/byte.h" +#include "libipc/concur/intrusive_stack.h" +#include "libipc/mem/central_cache_allocator.h" + +namespace ipc { +namespace mem { + +/** + * \brief The block type. + * \tparam BlockSize specifies the memory block size +*/ +template +union block { + block *next; + alignas(std::max_align_t) std::array storage; +}; + +/** + * \brief A fixed-length memory block central cache pool. + * \tparam BlockT specifies the memory block type + */ +template +class central_cache_pool { + + /// \brief The block type, which should be a union of a pointer and a storage. + using block_t = BlockT; + /// \brief The chunk type, which is an array of blocks. + using chunk_t = std::array; + /// \brief The node type, which is used to store the block pointer. + using node_t = typename concur::intrusive_stack::node; + + /// \brief The central cache stack. + concur::intrusive_stack cached_; + concur::intrusive_stack aqueired_; + + central_cache_pool() noexcept = default; + +public: + block_t *aqueire() noexcept { + auto *n = cached_.pop(); + if (n != nullptr) { + aqueired_.push(n); + return n->value; + } + auto *chunk = central_cache_allocator().construct(); + if (chunk == nullptr) { + return nullptr; + } + for (std::size_t i = 0; i < BlockPoolExpansion - 1; ++i) { + (*chunk)[i].next = &(*chunk)[i + 1]; + } + chunk->back().next = nullptr; + return chunk->data(); + } + + void release(block_t *p) noexcept { + if (p == nullptr) return; + auto *a = aqueired_.pop(); + if (a == nullptr) { + a = central_cache_allocator().construct(); + if (a == nullptr) return; + } + a->value = p; + cached_.push(a); + } + + /// \brief Get the singleton instance. + static central_cache_pool &instance() noexcept { + static central_cache_pool pool; + return pool; + } +}; + +/// \brief A fixed-length memory block central cache pool with no default expansion size. +template +class central_cache_pool { + + /// \brief The block type, which should be a union of a pointer and a storage. + using block_t = BlockT; + /// \brief The node type, which is used to store the block pointer. + using node_t = typename concur::intrusive_stack::node; + + /// \brief The central cache stack. + concur::intrusive_stack cached_; + concur::intrusive_stack aqueired_; + + central_cache_pool() noexcept = default; + +public: + block_t *aqueire() noexcept { + auto *n = cached_.pop(); + if (n != nullptr) { + aqueired_.push(n); + return n->value; + } + // For pools with no default expansion size, + // the central cache pool is only buffered, not allocated. + return nullptr; + } + + void release(block_t *p) noexcept { + if (p == nullptr) return; + auto *a = aqueired_.pop(); + if (a == nullptr) { + a = central_cache_allocator().construct(); + if (a == nullptr) return; + } + a->value = p; + cached_.push(a); + } + + /// \brief Get the singleton instance. + static central_cache_pool &instance() noexcept { + static central_cache_pool pool; + return pool; + } +}; + +} // namespace mem +} // namespace ipc diff --git a/src/libipc/mem/central_cache_allocator.cpp b/src/libipc/mem/central_cache_allocator.cpp new file mode 100644 index 0000000..58be38b --- /dev/null +++ b/src/libipc/mem/central_cache_allocator.cpp @@ -0,0 +1,47 @@ + +#include +#include +#include + +#include "libipc/def.h" +#include "libipc/imp/detect_plat.h" +#include "libipc/imp/byte.h" +#include "libipc/mem/polymorphic_allocator.h" +#include "libipc/mem/memory_resource.h" + +namespace ipc { +namespace mem { + +class thread_safe_resource : public monotonic_buffer_resource { +public: + thread_safe_resource(span buffer) noexcept + : monotonic_buffer_resource(buffer) {} + + ~thread_safe_resource() noexcept { + LIBIPC_UNUSED std::lock_guard lock(mutex_); + monotonic_buffer_resource::release(); + } + + void *allocate(std::size_t bytes, std::size_t alignment) noexcept { + LIBIPC_UNUSED std::lock_guard lock(mutex_); + return monotonic_buffer_resource::allocate(bytes, alignment); + } + + void deallocate(void *p, std::size_t bytes, std::size_t alignment) noexcept { + LIBIPC_UNUSED std::lock_guard lock(mutex_); + monotonic_buffer_resource::deallocate(p, bytes, alignment); + } + +private: + std::mutex mutex_; +}; + +bytes_allocator ¢ral_cache_allocator() noexcept { + static std::array buf; + static thread_safe_resource res(buf); + static bytes_allocator a(&res); + return a; +} + +} // namespace mem +} // namespace ipc diff --git a/test/mem/test_mem_block_pool.cpp b/test/mem/test_mem_block_pool.cpp new file mode 100644 index 0000000..f38662e --- /dev/null +++ b/test/mem/test_mem_block_pool.cpp @@ -0,0 +1,32 @@ + +#include "test.h" + +#include + +#include "libipc/mem/block_pool.h" + +TEST(block_pool, ctor) { + ASSERT_TRUE ((std::is_default_constructible>::value)); + ASSERT_FALSE((std::is_copy_constructible>::value)); + ASSERT_TRUE ((std::is_move_constructible>::value)); + ASSERT_FALSE((std::is_copy_assignable>::value)); + ASSERT_FALSE((std::is_move_assignable>::value)); +} + +TEST(block_pool, allocate) { + std::vector v; + ipc::mem::block_pool<1, 1> pool; + for (int i = 0; i < 100; ++i) { + v.push_back(pool.allocate()); + } + for (void *p: v) { + ASSERT_FALSE(nullptr == p); + pool.deallocate(p); + } + for (int i = 0; i < 100; ++i) { + ASSERT_EQ(v[v.size() - i - 1], pool.allocate()); + } + for (void *p: v) { + pool.deallocate(p); + } +} diff --git a/test/mem/test_mem_central_cache_allocator.cpp b/test/mem/test_mem_central_cache_allocator.cpp new file mode 100644 index 0000000..0ab67aa --- /dev/null +++ b/test/mem/test_mem_central_cache_allocator.cpp @@ -0,0 +1,15 @@ + +#include "test.h" + +#include + +#include "libipc/mem/central_cache_allocator.h" + +TEST(central_cache_allocator, allocate) { + auto &a = ipc::mem::central_cache_allocator(); + ASSERT_FALSE(nullptr == a.allocate(1)); + ASSERT_FALSE(nullptr == a.allocate(10)); + ASSERT_FALSE(nullptr == a.allocate(100)); + ASSERT_FALSE(nullptr == a.allocate(1000)); + ASSERT_FALSE(nullptr == a.allocate(10000)); +} diff --git a/test/mem/test_mem_central_cache_pool.cpp b/test/mem/test_mem_central_cache_pool.cpp new file mode 100644 index 0000000..e6cf19b --- /dev/null +++ b/test/mem/test_mem_central_cache_pool.cpp @@ -0,0 +1,44 @@ + +#include "test.h" + +#include + +#include "libipc/mem/central_cache_pool.h" + +TEST(central_cache_pool, ctor) { + ASSERT_FALSE((std::is_default_constructible, 1>>::value)); + ASSERT_FALSE((std::is_copy_constructible, 1>>::value)); + ASSERT_FALSE((std::is_move_constructible, 1>>::value)); + ASSERT_FALSE((std::is_copy_assignable, 1>>::value)); + ASSERT_FALSE((std::is_move_assignable, 1>>::value)); + { + auto &pool = ipc::mem::central_cache_pool, 1>::instance(); + ipc::mem::block<1> *b1 = pool.aqueire(); + ASSERT_FALSE(nullptr == b1); + ASSERT_TRUE (nullptr == b1->next); + pool.release(b1); + ipc::mem::block<1> *b2 = pool.aqueire(); + EXPECT_EQ(b1, b2); + ipc::mem::block<1> *b3 = pool.aqueire(); + ASSERT_FALSE(nullptr == b3); + ASSERT_TRUE (nullptr == b3->next); + EXPECT_NE(b1, b3); + } + { + auto &pool = ipc::mem::central_cache_pool, 2>::instance(); + ipc::mem::block<1> *b1 = pool.aqueire(); + ASSERT_FALSE(nullptr == b1); + ASSERT_FALSE(nullptr == b1->next); + ASSERT_TRUE (nullptr == b1->next->next); + pool.release(b1); + ipc::mem::block<1> *b2 = pool.aqueire(); + EXPECT_EQ(b1, b2); + ipc::mem::block<1> *b3 = pool.aqueire(); + EXPECT_NE(b1, b3); + ipc::mem::block<1> *b4 = pool.aqueire(); + ASSERT_FALSE(nullptr == b4); + ASSERT_FALSE(nullptr == b4->next); + ASSERT_TRUE (nullptr == b4->next->next); + EXPECT_NE(b1, b4); + } +}