Add block_pool

This commit is contained in:
mutouyun 2025-01-21 17:37:53 +08:00 committed by 木头云
parent dd8307e81a
commit 0b33859d33
7 changed files with 411 additions and 0 deletions

View File

@ -0,0 +1,121 @@
/**
* \file libipc/block_pool.h
* \author mutouyun (orz@orzz.org)
* \brief The fixed-length memory block pool.
*/
#pragma once
#include <cstddef>
#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 <std::size_t BlockSize, std::size_t BlockPoolExpansion>
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 <std::size_t BlockSize, std::size_t BlockPoolExpansion>
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<block_t, 0>;
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<block_t *>(p);
b->next = cursor_;
cursor_ = b;
}
private:
block_t *cursor_;
};
/// \brief A block pool for a block of memory of a specific size.
template <std::size_t BlockSize, std::size_t BlockPoolExpansion>
class block_pool {
/// \brief The block type.
using block_t = block<BlockSize>;
/// \brief The central cache pool type.
using central_cache_pool_t = central_cache_pool<block_t, BlockPoolExpansion>;
/// \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<block_t *>(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<block_t *>(p);
b->next = cursor_;
cursor_ = b;
}
private:
block_t *cursor_;
};
} // namespace mem
} // namespace ipc

View File

@ -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 &central_cache_allocator() noexcept;
} // namespace mem
} // namespace ipc

View File

@ -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 <cstddef>
#include <deque>
#include <utility>
#include <array>
#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 <std::size_t BlockSize>
union block {
block *next;
alignas(std::max_align_t) std::array<byte, BlockSize> storage;
};
/**
* \brief A fixed-length memory block central cache pool.
* \tparam BlockT specifies the memory block type
*/
template <typename BlockT, std::size_t BlockPoolExpansion>
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<block_t, BlockPoolExpansion>;
/// \brief The node type, which is used to store the block pointer.
using node_t = typename concur::intrusive_stack<block_t *>::node;
/// \brief The central cache stack.
concur::intrusive_stack<block_t *> cached_;
concur::intrusive_stack<block_t *> 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<chunk_t>();
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<node_t>();
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 <typename BlockT>
class central_cache_pool<BlockT, 0> {
/// \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<block_t *>::node;
/// \brief The central cache stack.
concur::intrusive_stack<block_t *> cached_;
concur::intrusive_stack<block_t *> 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<node_t>();
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

View File

@ -0,0 +1,47 @@
#include <mutex>
#include <array>
#include <cstddef>
#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<byte> buffer) noexcept
: monotonic_buffer_resource(buffer) {}
~thread_safe_resource() noexcept {
LIBIPC_UNUSED std::lock_guard<std::mutex> lock(mutex_);
monotonic_buffer_resource::release();
}
void *allocate(std::size_t bytes, std::size_t alignment) noexcept {
LIBIPC_UNUSED std::lock_guard<std::mutex> 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<std::mutex> lock(mutex_);
monotonic_buffer_resource::deallocate(p, bytes, alignment);
}
private:
std::mutex mutex_;
};
bytes_allocator &central_cache_allocator() noexcept {
static std::array<byte, central_cache_default_size> buf;
static thread_safe_resource res(buf);
static bytes_allocator a(&res);
return a;
}
} // namespace mem
} // namespace ipc

View File

@ -0,0 +1,32 @@
#include "test.h"
#include <utility>
#include "libipc/mem/block_pool.h"
TEST(block_pool, ctor) {
ASSERT_TRUE ((std::is_default_constructible<ipc::mem::block_pool<1, 1>>::value));
ASSERT_FALSE((std::is_copy_constructible<ipc::mem::block_pool<1, 1>>::value));
ASSERT_TRUE ((std::is_move_constructible<ipc::mem::block_pool<1, 1>>::value));
ASSERT_FALSE((std::is_copy_assignable<ipc::mem::block_pool<1, 1>>::value));
ASSERT_FALSE((std::is_move_assignable<ipc::mem::block_pool<1, 1>>::value));
}
TEST(block_pool, allocate) {
std::vector<void *> 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);
}
}

View File

@ -0,0 +1,15 @@
#include "test.h"
#include <utility>
#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));
}

View File

@ -0,0 +1,44 @@
#include "test.h"
#include <utility>
#include "libipc/mem/central_cache_pool.h"
TEST(central_cache_pool, ctor) {
ASSERT_FALSE((std::is_default_constructible<ipc::mem::central_cache_pool<ipc::mem::block<1>, 1>>::value));
ASSERT_FALSE((std::is_copy_constructible<ipc::mem::central_cache_pool<ipc::mem::block<1>, 1>>::value));
ASSERT_FALSE((std::is_move_constructible<ipc::mem::central_cache_pool<ipc::mem::block<1>, 1>>::value));
ASSERT_FALSE((std::is_copy_assignable<ipc::mem::central_cache_pool<ipc::mem::block<1>, 1>>::value));
ASSERT_FALSE((std::is_move_assignable<ipc::mem::central_cache_pool<ipc::mem::block<1>, 1>>::value));
{
auto &pool = ipc::mem::central_cache_pool<ipc::mem::block<1>, 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<ipc::mem::block<1>, 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);
}
}