From ad12fe8c86c2b5aa972d9bdbba1a2770b2ae26a9 Mon Sep 17 00:00:00 2001 From: jmd Date: Wed, 9 Oct 2024 11:54:24 -0700 Subject: [PATCH] [util] Changed my mind on allocator, switch to a polymorphic strategy fix: Change MemoryResource to be polymorphic add: Allocator for adapting to std::allocator interface fix: massively reduce dependencies. --- lib/nw/CMakeLists.txt | 1 + lib/nw/config.hpp | 9 +-- lib/nw/util/Allocator.cpp | 23 ++++++ lib/nw/util/Allocator.hpp | 68 ++++++++++++++++ lib/nw/util/memory.cpp | 4 +- lib/nw/util/memory.hpp | 166 +++++++++----------------------------- tests/util_memory.cpp | 6 +- 7 files changed, 139 insertions(+), 138 deletions(-) create mode 100644 lib/nw/util/Allocator.cpp create mode 100644 lib/nw/util/Allocator.hpp diff --git a/lib/nw/CMakeLists.txt b/lib/nw/CMakeLists.txt index d99203365..0a7ce90fd 100644 --- a/lib/nw/CMakeLists.txt +++ b/lib/nw/CMakeLists.txt @@ -108,6 +108,7 @@ add_library(nw STATIC serialization/GffBuilder.cpp serialization/Serialization.cpp + util/Allocator.cpp util/base64.cpp util/ByteArray.cpp util/compression.cpp diff --git a/lib/nw/config.hpp b/lib/nw/config.hpp index 2fe771649..bb136fccb 100644 --- a/lib/nw/config.hpp +++ b/lib/nw/config.hpp @@ -7,24 +7,23 @@ #include ROLLNW_CUSTOM_CONFIGURATION_FILE #else -#include "util/memory.hpp" +#include "util/Allocator.hpp" #include #include namespace nw { -// Strings. +// Strings using String = std::string; using StringView = std::string_view; -using PoolString = std::basic_string, PoolAllocator>; -using ScopeString = std::basic_string, ScopeAllocator>; +using PString = std::basic_string, Allocator>; // Vector template using Vector = std::vector; template -using ScopeVector = std::vector>; +using PVector = std::vector>; } // namespace nw diff --git a/lib/nw/util/Allocator.cpp b/lib/nw/util/Allocator.cpp new file mode 100644 index 000000000..18bd2d450 --- /dev/null +++ b/lib/nw/util/Allocator.cpp @@ -0,0 +1,23 @@ +#include "Allocator.hpp" +#include "memory.hpp" + +namespace nw { +namespace detail { + +MemoryResourceInternal::MemoryResourceInternal(MemoryResource* resource) + : resource_{resource} +{ +} + +void* MemoryResourceInternal::allocate(size_t bytes, size_t alignment) +{ + return resource_->allocate(bytes, alignment); +} + +void MemoryResourceInternal::deallocate(void* p, size_t bytes, size_t alignment) +{ + resource_->deallocate(p, bytes, alignment); +} + +} // namespace detail +} // namespace nw diff --git a/lib/nw/util/Allocator.hpp b/lib/nw/util/Allocator.hpp new file mode 100644 index 000000000..325857319 --- /dev/null +++ b/lib/nw/util/Allocator.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include + +namespace nw { + +struct MemoryResource; + +namespace detail { +// Hide MemoryResource impl details +struct MemoryResourceInternal { + MemoryResourceInternal(MemoryResource* resoure); + void* allocate(size_t nbytes, size_t alignment = alignof(max_align_t)); + void deallocate(void* p, size_t bytes, size_t alignment = alignof(max_align_t)); + MemoryResource* resource_; +}; + +} + +/// Allocator adapter for MemoryResource, so this is a polymorphic allocator. +template +class Allocator { +public: + using value_type = T; + + Allocator(MemoryResource* resource) + : resource_(resource) + { + } + + template + Allocator(const Allocator& other) noexcept + : resource_(other.resource_) + { + } + + T* allocate(size_t n) + { + if (n == 0) { return nullptr; } + return static_cast(resource_.allocate(n * sizeof(T), alignof(T))); + } + + void deallocate(T* ptr, size_t size) noexcept + { + resource_.deallocate(ptr, size); + } + + template + struct rebind { + using other = Allocator; + }; + + // private: + detail::MemoryResourceInternal resource_; +}; + +template +bool operator==(const Allocator& a, const Allocator& b) noexcept +{ + return &a.resource_.resource_ == &b.resource_.resource_; +} + +template +bool operator!=(const Allocator& a, const Allocator& b) noexcept +{ + return !(a == b); +} +} // namespace nw diff --git a/lib/nw/util/memory.cpp b/lib/nw/util/memory.cpp index ea59e1fbb..7a2f9024a 100644 --- a/lib/nw/util/memory.cpp +++ b/lib/nw/util/memory.cpp @@ -137,7 +137,7 @@ MemoryScope::~MemoryScope() arena_->rewind(marker_); } -void* MemoryScope::alloc(size_t size, size_t alignment) +void* MemoryScope::allocate(size_t size, size_t alignment) { return arena_->allocate(size, alignment); } @@ -240,7 +240,7 @@ void* MemoryPool::allocate(size_t size, size_t alignment) return aligned; } -void MemoryPool::deallocate(void* ptr) +void MemoryPool::deallocate(void* ptr, size_t, size_t) { if (!ptr) return; detail::PoolHeader* header = reinterpret_cast(static_cast(ptr) - sizeof(detail::PoolHeader)); diff --git a/lib/nw/util/memory.hpp b/lib/nw/util/memory.hpp index 254c6e68b..c983d4b92 100644 --- a/lib/nw/util/memory.hpp +++ b/lib/nw/util/memory.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../config.hpp" #include "../log.hpp" #include "templates.hpp" @@ -30,6 +31,14 @@ constexpr std::uint64_t GB(std::uint64_t gb) return gb * 1024ULL * 1024ULL * 1024ULL; } +/// Abstracts some memory resource, i.e. global malloc/free +struct MemoryResource { + virtual ~MemoryResource() = default; + + virtual void* allocate(size_t bytes, size_t alignment = alignof(std::max_align_t)) = 0; + virtual void deallocate(void* p, size_t bytes, size_t alignment = alignof(std::max_align_t)) = 0; +}; + struct MemoryBlock { uint8_t* block = nullptr; size_t position = 0; @@ -46,7 +55,7 @@ struct MemoryMarker { }; /// A growable Memory Arena -struct MemoryArena { +struct MemoryArena : MemoryResource { MemoryArena(size_t blockSize = 1024); MemoryArena(const MemoryArena&) = delete; MemoryArena(MemoryArena&&) = default; @@ -58,6 +67,9 @@ struct MemoryArena { /// Allocate memory memory on the arena. void* allocate(size_t size, size_t alignment = alignof(max_align_t)); + /// No-op + virtual void deallocate(void*, size_t, size_t) { }; + /// Gets the current point in the allocator MemoryMarker current(); @@ -68,7 +80,7 @@ struct MemoryArena { void rewind(MemoryMarker marker); private: - std::vector blocks_; + Vector blocks_; size_t current_block_ = 0; size_t size_ = 0; @@ -78,14 +90,14 @@ struct MemoryArena { // This is very simple and naive. template struct ObjectPool { - ObjectPool(MemoryArena* arena) - : arena_{arena} + ObjectPool(MemoryResource* resource) + : resource_{resource} { } T* allocate() { - if (!arena_) { return nullptr; } + if (!resource_) { return nullptr; } if (free_list_.empty()) { allocate_chunk(); } auto result = free_list_.top(); @@ -96,13 +108,13 @@ struct ObjectPool { void clear() { - free_list_ = std::stack>{}; + free_list_ = std::stack>{}; chunks_.clear(); } void free(T* object) { - if (!arena_) { return; } + if (!resource_) { return; } object->~T(); free_list_.push(object); @@ -110,18 +122,18 @@ struct ObjectPool { void allocate_chunk() { - if (arena_ == nullptr) { return; } + if (resource_ == nullptr) { return; } - T* chunk = static_cast(arena_->allocate(sizeof(T) * chunk_size, alignof(T))); + T* chunk = static_cast(resource_->allocate(sizeof(T) * chunk_size, alignof(T))); CHECK_F(!!chunk, "Unable to allocate chunk of size {}", sizeof(T) * chunk_size); for (size_t i = 0; i < chunk_size; ++i) { free_list_.push(&chunk[i]); } } - MemoryArena* arena_{chunk_size * sizeof(T)}; - std::stack> free_list_; - std::vector chunks_; + MemoryResource* resource_; + std::stack> free_list_; + Vector chunks_; }; namespace detail { @@ -148,7 +160,7 @@ struct FinalizedObject { // == MemoryScope ======================================================================= // ====================================================================================== -struct MemoryScope { +struct MemoryScope : public MemoryResource { MemoryScope(MemoryArena* arena); MemoryScope(const MemoryScope&) = delete; MemoryScope(MemoryScope&& other); @@ -158,14 +170,17 @@ struct MemoryScope { MemoryScope& operator=(MemoryScope&& other); /// Allocates ``size`` bytes with ``alignment`` - void* alloc(size_t size, size_t alignment = alignof(max_align_t)); + virtual void* allocate(size_t bytes, size_t alignment = alignof(max_align_t)) override; + + /// No-op + virtual void deallocate(void*, size_t, size_t) override { }; /// Allocates a non-trivial object and stores pointer to destructor that is run when scope exits. template T* alloc_obj(Args&&... args) { static_assert(!(std::is_standard_layout_v && std::is_trivial_v), "Use alloc_pod for POD types"); - void* mem = alloc(sizeof(detail::FinalizedObject), alignof(detail::FinalizedObject)); + void* mem = allocate(sizeof(detail::FinalizedObject), alignof(detail::FinalizedObject)); auto fo = static_cast*>(mem); fo->f.fn = &detail::destructor; fo->f.next = finalizers_; // last in, first destructed. @@ -178,7 +193,7 @@ struct MemoryScope { T* alloc_pod() { static_assert(std::is_standard_layout_v && std::is_trivial_v, "Use alloc_obj for non-trivial types"); - void* mem = alloc(sizeof(T), alignof(T)); + void* mem = allocate(sizeof(T), alignof(T)); return new (mem) T(); } @@ -188,68 +203,6 @@ struct MemoryScope { detail::Finalizer* finalizers_ = nullptr; }; -template -class ScopeAllocator { -public: - using value_type = T; - - // Constructor accepting MemoryScope reference - ScopeAllocator(MemoryScope* scope) - : scope_(scope) - { - } - - // Copy constructor (needed by allocator interface) - template - ScopeAllocator(const ScopeAllocator& other) noexcept - : scope_(other.scope_) - { - } - - T* allocate(std::size_t n) - { - if (n == 0) - return nullptr; - return static_cast(scope_->alloc(n * sizeof(T), alignof(T))); - } - - void deallocate(T*, std::size_t) noexcept - { - } - - template - void construct(U* p, Args&&... args) - { - ::new (static_cast(p)) U(std::forward(args)...); - } - - template - void destroy(U* p) noexcept - { - p->~U(); - } - - template - struct rebind { - using other = ScopeAllocator; - }; - - // private: - MemoryScope* scope_; -}; - -template -bool operator==(const ScopeAllocator& a, const ScopeAllocator& b) noexcept -{ - return &a.scope_ == &b.scope_; -} - -template -bool operator!=(const ScopeAllocator& a, const ScopeAllocator& b) noexcept -{ - return !(a == b); -} - // == MemoryPool ======================================================================== // ====================================================================================== @@ -272,67 +225,24 @@ struct PoolBlock { // private: size_t block_size_; size_t block_count_; - std::vector blocks_; - std::vector free_list_; + Vector blocks_; + Vector free_list_; void expand(size_t count); }; } // namespace detail -struct MemoryPool { +struct MemoryPool : public MemoryResource { MemoryPool(size_t max_size, size_t count); - void* allocate(size_t size, size_t alignment = alignof(max_align_t)); - void deallocate(void* ptr); + + virtual void* allocate(size_t bytes, size_t alignment = alignof(max_align_t)) override; + virtual void deallocate(void* ptr, size_t bytes = 0, size_t alignment = alignof(std::max_align_t)) override; // private: - std::vector pools_; + Vector pools_; size_t max_size_; size_t count_; }; -// Standard allocator wrapper for MemoryPool -template -class PoolAllocator { -public: - using value_type = T; - - PoolAllocator(MemoryPool* pool) - : pool_(pool) - { - } - - /// Allocates memory for n objects of type T - T* allocate(size_t n) - { - size_t bytes = n * sizeof(T); - return static_cast(pool_->allocate(bytes, alignof(T))); - } - - /// Deallocate memory for n objects of type T - void deallocate(T* p, size_t) - { - pool_->deallocate(static_cast(p)); - } - - template - struct rebind { - using other = PoolAllocator; - }; - - // Comparison operators for allocators - bool operator==(const PoolAllocator& other) const noexcept - { - return &pool_ == &other.pool_; - } - - bool operator!=(const PoolAllocator& other) const noexcept - { - return !(*this == other); - } - -private: - MemoryPool* pool_; -}; - } // namespace nw diff --git a/tests/util_memory.cpp b/tests/util_memory.cpp index 132d11e6f..cbf36d9ca 100644 --- a/tests/util_memory.cpp +++ b/tests/util_memory.cpp @@ -37,7 +37,7 @@ TEST(Memory, Scope) { nw::MemoryScope scope(&arena); - scope.alloc(60); + scope.allocate(60); EXPECT_NE(current, arena.current()); } EXPECT_EQ(current, arena.current()); @@ -65,7 +65,7 @@ TEST(Memory, Scope) result = false; { nw::MemoryScope scope(&arena); - auto vec = scope.alloc_obj>>(&scope); + auto vec = scope.alloc_obj>>(&scope); vec->push_back(TestStruct(&result)); EXPECT_NE(current, arena.current()); } @@ -79,7 +79,7 @@ TEST(Memory, Pool) size_t free = mp.pools_[3].free_list_.size(); { EXPECT_EQ(32 + 16, mp.pools_[3].block_size()); - nw::PoolString s("Hello World, this is a test.", &mp); + nw::PString s("Hello World, this is a test.", &mp); EXPECT_EQ(free - 1, mp.pools_[3].free_list_.size()); } EXPECT_EQ(free, mp.pools_[3].free_list_.size());