From 91331d2ae422d44bf34b996e14593473b0668911 Mon Sep 17 00:00:00 2001 From: pknowles Date: Sat, 1 Jun 2024 14:55:30 -0700 Subject: [PATCH 1/2] cleanup and split allocator.hpp to separate files Fixes a bug where the arena's m_end was not updated after reallocating --- README.md | 10 +- include/decodeless/allocator.hpp | 247 ++++-------------- include/decodeless/allocator_concepts.hpp | 71 +++++ include/decodeless/allocator_construction.hpp | 128 +++++++++ include/decodeless/pmr_allocator.hpp | 7 +- test/src/allocator.cpp | 62 ++--- 6 files changed, 299 insertions(+), 226 deletions(-) create mode 100644 include/decodeless/allocator_concepts.hpp create mode 100644 include/decodeless/allocator_construction.hpp diff --git a/README.md b/README.md index a7fdf3a..10ded07 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,11 @@ decodeless::linear_memory_resource memory(1024); std::span array = decodeless::create::array(memory, {1, 3, 6, 10, 15}); EXPECT_EQ(array.size(), 5); EXPECT_EQ(array[4], 15); -EXPECT_EQ(memory.bytesAllocated(), sizeof(int) * 5); +EXPECT_EQ(memory.size(), sizeof(int) * 5); double* alignedDouble = decodeless::create::object(memory, 42.0); EXPECT_EQ(*alignedDouble, 42.0); -EXPECT_EQ(memory.bytesAllocated(), sizeof(int) * 5 + sizeof(double) + 4); +EXPECT_EQ(memory.size(), sizeof(int) * 5 + sizeof(double) + 4); ``` Using the polymorphic allocator: @@ -68,9 +68,13 @@ decodeless::pmr_linear_memory_resource res(100); std::pmr::polymorphic_allocator alloc(&res); // interface abstraction std::span bytes = decodeless::create::array(alloc, 10); EXPECT_EQ(bytes.size(), 10); -EXPECT_EQ(res.bytesAllocated(), 10); +EXPECT_EQ(res.size(), 10); ``` +## Dependencies + +None. Just needs C++20. + ## Cmake Integration This is a header only library with no dependencies other than C++20. A diff --git a/include/decodeless/allocator.hpp b/include/decodeless/allocator.hpp index 32c285f..a212638 100644 --- a/include/decodeless/allocator.hpp +++ b/include/decodeless/allocator.hpp @@ -2,70 +2,15 @@ #pragma once -#include #include #include +#include #include -#include -#include - -#if __has_include() - #include -#endif namespace decodeless { -template -concept memory_resource = requires(Resource& resource) { - { - resource.allocate(std::declval(), std::declval()) - } -> std::same_as; - { - resource.deallocate(std::declval(), - std::declval()) - } -> std::same_as; -}; - -template -concept realloc_memory_resource = memory_resource && requires(Resource& resource) { - { - resource.reallocate(std::declval(), std::declval(), - std::declval()) - } -> std::same_as; -}; - -template -concept allocator = requires(Allocator& allocator, typename Allocator::value_type) { - { - allocator.allocate(std::declval()) - } -> std::same_as; - { - allocator.deallocate(std::declval(), - std::declval()) - } -> std::same_as; -}; - -template -concept realloc_allocator = - allocator && requires(Allocator& allocator, typename Allocator::value_type) { - { - allocator.reallocate(std::declval(), - std::declval()) - } -> std::same_as; - }; - -template -concept memory_resource_or_allocator = (memory_resource || allocator); - -template -concept realloc_resource_or_allocator = - realloc_memory_resource || realloc_allocator; - -template -concept has_max_size = memory_resource_or_allocator && requires(ResOrAlloc allocator) { - { allocator.max_size() } -> std::same_as; -}; - +// Utility for a linear_memory_resource backed by either a memory +// resource or an allocator template std::byte* allocate_bytes(ResOrAlloc& resOrAlloc, size_t bytes) { if constexpr (memory_resource) @@ -74,6 +19,8 @@ std::byte* allocate_bytes(ResOrAlloc& resOrAlloc, size_t bytes) { return resOrAlloc.allocate(bytes); } +// Reallocate utility for a linear_memory_resource backed by either a memory +// resource or an allocator template std::byte* reallocate_bytes(ResOrAlloc& resOrAlloc, std::byte* original, size_t size) { if constexpr (memory_resource) @@ -83,9 +30,6 @@ std::byte* reallocate_bytes(ResOrAlloc& resOrAlloc, std::byte* original, size_t return resOrAlloc.reallocate(original, size); } -template -concept trivially_destructible = std::is_trivially_destructible_v; - // A possibly-growable local linear arena allocator. // - growable: The backing allocation may grow if it has reallocate() and the // call returns the same address. @@ -95,9 +39,8 @@ concept trivially_destructible = std::is_trivially_destructible_v; // objects should be created from this. // - arena: Allocations come from a single blob/pool and when it is exhausted // std::bad_alloc is thrown (unless a reallocate() is possible). -// Backed by either a STL style allocator (typically just a pointer) or a -// concrete memory resource, although both need a reallocate() and max_size() -// call to enable growing. +// Backed by either a STL style allocator or a concrete memory resource, +// although both need a reallocate() and max_size() call to enable growing. // NOTE: currently expects std::byte allocators - use rebind_alloc from // std::allocator_traits if needed template > @@ -107,24 +50,24 @@ class linear_memory_resource { using parent_allocator = ParentAllocator; linear_memory_resource(size_t initialSize = INITIAL_SIZE, - const ParentAllocator& parentAllocator = ParentAllocator()) + const ParentAllocator& parent = ParentAllocator()) requires allocator - : m_parentAllocator(parentAllocator) - , m_begin(allocate_bytes(m_parentAllocator, initialSize)) + : m_parent(parent) + , m_begin(allocate_bytes(m_parent, initialSize)) , m_next(reinterpret_cast(m_begin)) , m_end(reinterpret_cast(m_begin) + initialSize) {} - linear_memory_resource(size_t initialSize, ParentAllocator&& parentAllocator) + linear_memory_resource(size_t initialSize, ParentAllocator&& parent) requires memory_resource - : m_parentAllocator(std::move(parentAllocator)) - , m_begin(allocate_bytes(m_parentAllocator, initialSize)) + : m_parent(std::move(parent)) + , m_begin(allocate_bytes(m_parent, initialSize)) , m_next(reinterpret_cast(m_begin)) , m_end(reinterpret_cast(m_begin) + initialSize) {} linear_memory_resource() = delete; linear_memory_resource(const linear_memory_resource& other) = delete; linear_memory_resource(linear_memory_resource&& other) noexcept = default; - ~linear_memory_resource() { m_parentAllocator.deallocate(m_begin, bytesAllocated()); } + ~linear_memory_resource() { m_parent.deallocate(m_begin, capacity()); } linear_memory_resource& operator=(const linear_memory_resource& other) = delete; linear_memory_resource& operator=(linear_memory_resource&& other) noexcept = default; @@ -140,22 +83,23 @@ class linear_memory_resource { if constexpr (realloc_resource_or_allocator) { // Allocate the larger of double the existing arena or enough to // fit what was just requested. - size_t newSize = std::max(bytesAllocated(), 2 * bytesReserved()); + size_t newSize = std::max(size(), 2 * capacity()); // If double the reservation would overflow the backing // allocator, allocate exactly the maximum. if constexpr (has_max_size) { - if (newSize > m_parentAllocator.max_size() && - bytesAllocated() < m_parentAllocator.max_size()) { - newSize = m_parentAllocator.max_size(); + if (newSize > m_parent.max_size() && size() < m_parent.max_size()) { + newSize = m_parent.max_size(); } } // Verify the reallocation produced the same address. - std::byte* addr = reallocate_bytes(m_parentAllocator, m_begin, newSize); + std::byte* addr = reallocate_bytes(m_parent, m_begin, newSize); if (addr != m_begin) { throw std::bad_alloc(); } + + m_end = reinterpret_cast(m_begin) + newSize; } else { throw std::bad_alloc(); } @@ -163,30 +107,48 @@ class linear_memory_resource { return reinterpret_cast(result); } + // Deallocates memory. This operation is a no-op for linear_memory_resource + // as individual deallocations are not supported. constexpr void deallocate(void* p, std::size_t bytes) { // Do nothing (void)p; (void)bytes; } - size_t bytesAllocated() const { return m_next - reinterpret_cast(m_begin); } - size_t bytesReserved() const { return m_end - reinterpret_cast(m_begin); } - void reset() { m_next = reinterpret_cast(m_begin); } + // Clear all allocations to begin allocating from scratch, invalidating all + // previously allocated memory. + void reset() { m_next = reinterpret_cast(m_begin); } + + // Reallocate the parent allocation to exactly the size of all current + // allocations. + void truncate() + requires realloc_resource_or_allocator + { + std::byte* addr = reallocate_bytes(m_parent, m_begin, size()); + if (addr != m_begin) { + throw std::bad_alloc(); + } + m_end = m_next; + } // Returns a pointer to the arena/parent allocation. - void* arena() const { return reinterpret_cast(m_begin); } + void* data() const { return reinterpret_cast(m_begin); } -protected: - ParentAllocator m_parentAllocator; + // Returns the total number of bytes allocated within the arena + size_t size() const { return m_next - reinterpret_cast(m_begin); } + + // Returns the size of the arena/parent allocation + size_t capacity() const { return m_end - reinterpret_cast(m_begin); } private: + ParentAllocator m_parent; std::byte* m_begin; uintptr_t m_next; uintptr_t m_end; }; -// Workaround copyable STL allocators by passing around a pointer/reference to -// the object with state. +// Stateful STL-compatible allocator adaptor that holds a pointer to the +// concrete memory resource template class memory_resource_ref { public: @@ -209,6 +171,14 @@ class memory_resource_ref { return m_resource->deallocate(static_cast(p), n); } + bool operator==(const memory_resource_ref& other) const { + return m_resource == other.m_resource; + } + + bool operator!=(const memory_resource_ref& other) const { + return m_resource != other.m_resource; + } + resource_type& resource() const { return *m_resource; } // Needed by msvc @@ -221,113 +191,10 @@ class memory_resource_ref { resource_type* m_resource; }; -// STL compatible allocator with an implicit constructor from -// linear_memory_resource. Emphasizes why std::pmr is a thing - ParentAllocator -// shouldn't affect the type. +// STL compatible allocator with an implicit linear_memory_resource memory +// resource. The need for this emphasizes why std::pmr is a thing - the +// MemoryResource would ideally not affect the type. template > using linear_allocator = memory_resource_ref; -namespace create { - -// Utility calls to construct objects from a decodeless memory resource -namespace from_resource { - -template -T* object(MemoryResource& memoryResource, const T& init) { - return std::construct_at(linear_allocator(memoryResource).allocate(1), - init); -}; - -template -T* object(MemoryResource& memoryResource, Args&&... args) { - return std::construct_at(linear_allocator(memoryResource).allocate(1), - std::forward(args)...); -}; - -template -std::span array(MemoryResource& memoryResource, size_t size) { - auto result = - std::span(linear_allocator(memoryResource).allocate(size), size); - for (auto& obj : result) - std::construct_at(&obj); - return result; -}; - -#ifdef __cpp_lib_ranges -template , - memory_resource MemoryResource> - requires std::convertible_to, T> -std::span array(MemoryResource& memoryResource, Range&& range) { - auto size = std::ranges::size(range); - auto result = - std::span(linear_allocator(memoryResource).allocate(size), size); - auto out = result.begin(); - for (auto& in : range) - std::construct_at(&*out++, in); - return result; -}; - -// Overload to deduce T from the Range type. Convenient but not always desired -template -auto array(MemoryResource& memoryResource, Range&& range) { - return array, Range, MemoryResource>( - memoryResource, std::forward(range)); -} -#endif - -} // namespace from_resource - -// Utility calls to construct objects from an STL compatible allocator -namespace from_allocator { - -template -using allocator_rebind_t = typename std::allocator_traits::template rebind_alloc; - -template -T* object(const Allocator& allocator, const T& init) { - return std::construct_at(allocator_rebind_t(allocator).allocate(1), init); -}; - -template -T* object(const Allocator& allocator, Args&&... args) { - return std::construct_at(allocator_rebind_t(allocator).allocate(1), - std::forward(args)...); -}; - -template -std::span array(const Allocator& allocator, size_t size) { - auto result = std::span(allocator_rebind_t(allocator).allocate(size), size); - for (auto& obj : result) - std::construct_at(&obj); - return result; -}; - -#ifdef __cpp_lib_ranges -template , - allocator Allocator> - requires std::convertible_to, T> -std::span array(const Allocator& allocator, Range&& range) { - auto size = std::ranges::size(range); - auto result = std::span(allocator_rebind_t(allocator).allocate(size), size); - auto out = result.begin(); - for (auto& in : range) - std::construct_at(&*out++, in); - return result; -}; - -// Overload to deduce T from the Range type. Convenient but not always desired -template -auto array(const Allocator& allocator, Range&& range) { - return array, Range, Allocator>(allocator, - std::forward(range)); -} -#endif - -} // namespace from_allocator - -using namespace from_resource; -using namespace from_allocator; - -} // namespace create - } // namespace decodeless diff --git a/include/decodeless/allocator_concepts.hpp b/include/decodeless/allocator_concepts.hpp new file mode 100644 index 0000000..11aa3da --- /dev/null +++ b/include/decodeless/allocator_concepts.hpp @@ -0,0 +1,71 @@ +// Copyright (c) 2024 Pyarelal Knowles, MIT License + +#pragma once + +#include +#include + +namespace decodeless { + +template +concept memory_resource = requires(Resource& resource) { + { + // allocate(size, alignment) + resource.allocate(std::declval(), std::declval()) + } -> std::same_as; + { + // deallocate(ptr, size) + resource.deallocate(std::declval(), std::declval()) + } -> std::same_as; +}; + +template +concept realloc_memory_resource = memory_resource && requires(Resource& resource) { + { + // reallocate(ptr, size, alignment) + resource.reallocate(std::declval(), std::declval(), + std::declval()) + } -> std::same_as; +}; + +template +concept allocator = requires(Allocator& allocator, typename Allocator::value_type) { + { + // allocate(size) + allocator.allocate(std::declval()) + } -> std::same_as; + { + // deallocate(ptr, size) + allocator.deallocate(std::declval(), + std::declval()) + } -> std::same_as; +}; + +template +concept realloc_allocator = + allocator && requires(Allocator& allocator, typename Allocator::value_type) { + { + // reallocate(ptr, size) + allocator.reallocate(std::declval(), + std::declval()) + } -> std::same_as; + }; + +// Facilitate decodeless::linear_memory_resource backed by either a memory +// resource or a STL style allocator +template +concept memory_resource_or_allocator = (memory_resource || allocator); + +template +concept realloc_resource_or_allocator = + realloc_memory_resource || realloc_allocator; + +template +concept has_max_size = memory_resource_or_allocator && requires(ResOrAlloc allocator) { + { allocator.max_size() } -> std::same_as; +}; + +template +concept trivially_destructible = std::is_trivially_destructible_v; + +} // namespace decodeless diff --git a/include/decodeless/allocator_construction.hpp b/include/decodeless/allocator_construction.hpp new file mode 100644 index 0000000..737f1a5 --- /dev/null +++ b/include/decodeless/allocator_construction.hpp @@ -0,0 +1,128 @@ +// Copyright (c) 2024 Pyarelal Knowles, MIT License + +#pragma once + +#include +#include + +#if __has_include() + #include +#endif + +namespace decodeless { + +namespace create { + +// Utility calls to construct objects from a decodeless memory resource +namespace from_resource { + +// Copy constructs an object with implicit type deduction. +template +T* object(MemoryResource& memoryResource, const T& init) { + return std::construct_at( + reinterpret_cast(memoryResource.allocate(sizeof(T), alignof(T))), init); +}; + +// Construct an explicitly typed object with any arguments. +template +T* object(MemoryResource& memoryResource, Args&&... args) { + return std::construct_at( + reinterpret_cast(memoryResource.allocate(sizeof(T), alignof(T))), + std::forward(args)...); +}; + +// Default construct an array of 'size' objects. +template +std::span array(MemoryResource& memoryResource, size_t size) { + auto result = std::span( + reinterpret_cast(memoryResource.allocate(sizeof(T) * size, alignof(T))), size); + for (auto& obj : result) + std::construct_at(&obj); + return result; +}; + +#ifdef __cpp_lib_ranges +// Copy construct an array of objects from a range. +template , + memory_resource MemoryResource> + requires std::convertible_to, T> +std::span array(MemoryResource& memoryResource, Range&& range) { + auto size = std::ranges::size(range); + auto result = std::span( + reinterpret_cast(memoryResource.allocate(sizeof(T) * size, alignof(T))), size); + auto out = result.begin(); + for (auto& in : range) + std::construct_at(&*out++, in); + return result; +}; + +// Overload to deduce T from the Range type +template +auto array(MemoryResource& memoryResource, Range&& range) { + return array, Range, MemoryResource>( + memoryResource, std::forward(range)); +} +#endif + +} // namespace from_resource + +// Utility calls to construct objects from an STL compatible allocator +namespace from_allocator { + +// Shortcut to rebind an allocator to a different type +template +using allocator_rebind_t = typename std::allocator_traits::template rebind_alloc; + +// Copy constructs an object with implicit type deduction. +template +T* object(const Allocator& allocator, const T& init) { + return std::construct_at(allocator_rebind_t(allocator).allocate(1), init); +}; + +// Construct an explicitly typed object with any arguments. +template +T* object(const Allocator& allocator, Args&&... args) { + return std::construct_at(allocator_rebind_t(allocator).allocate(1), + std::forward(args)...); +}; + +// Default construct an array of 'size' objects. +template +std::span array(const Allocator& allocator, size_t size) { + auto result = std::span(allocator_rebind_t(allocator).allocate(size), size); + for (auto& obj : result) + std::construct_at(&obj); + return result; +}; + +#ifdef __cpp_lib_ranges +// Copy construct an array of objects from a range. +template , + allocator Allocator> + requires std::convertible_to, T> +std::span array(const Allocator& allocator, Range&& range) { + auto size = std::ranges::size(range); + auto result = std::span(allocator_rebind_t(allocator).allocate(size), size); + auto out = result.begin(); + for (auto& in : range) + std::construct_at(&*out++, in); + return result; +}; + +// Overload to deduce T from the Range type. +template +auto array(const Allocator& allocator, Range&& range) { + return array, Range, Allocator>(allocator, + std::forward(range)); +} +#endif + +} // namespace from_allocator + +// Add all construction calls to the decodeless::create namespace +using namespace from_resource; +using namespace from_allocator; + +} // namespace create + +} // namespace decodeless diff --git a/include/decodeless/pmr_allocator.hpp b/include/decodeless/pmr_allocator.hpp index c21505c..0dc40b2 100644 --- a/include/decodeless/pmr_allocator.hpp +++ b/include/decodeless/pmr_allocator.hpp @@ -43,10 +43,11 @@ class pmr_linear_memory_resource requires memory_resource : memory_resource_adapter>( initialSize, std::move(parentAllocator)) {} - size_t bytesAllocated() const { return this->backing_resource().bytesAllocated(); } - size_t bytesReserved() const { return this->backing_resource().bytesReserved(); } void reset() { this->backing_resource().reset(); } - void* arena() const { return this->backing_resource().arena(); } + void truncate() { this->backing_resource().truncate(); } + void* data() const { return this->backing_resource().data(); } + size_t size() const { return this->backing_resource().size(); } + size_t capacity() const { return this->backing_resource().capacity(); } }; } // namespace decodeless diff --git a/test/src/allocator.cpp b/test/src/allocator.cpp index a0d5da8..b0aeccb 100644 --- a/test/src/allocator.cpp +++ b/test/src/allocator.cpp @@ -1,10 +1,12 @@ // Copyright (c) 2024 Pyarelal Knowles, MIT License +#include #include #include #include #include #include +#include #include #include #include @@ -32,20 +34,20 @@ TEST(Allocate, Object) { // byte can be placed anywhere EXPECT_EQ(memory.allocate(sizeof(char), alignof(char)), reinterpret_cast(0)); - EXPECT_EQ(memory.bytesAllocated(), 1); + EXPECT_EQ(memory.size(), 1); // int after the byte must have 3 bytes padding, placed at 4 and taking 4 EXPECT_EQ(memory.allocate(sizeof(int), alignof(int)), reinterpret_cast(4)); - EXPECT_EQ(memory.bytesAllocated(), 8); + EXPECT_EQ(memory.size(), 8); // double after int must have 4 bytes padding, placed at 8, taking 8 more EXPECT_EQ(memory.allocate(sizeof(double), alignof(double)), reinterpret_cast(8)); - EXPECT_EQ(memory.bytesAllocated(), 16); + EXPECT_EQ(memory.size(), 16); // another byte to force some padding, together with another int won't fit - EXPECT_EQ(memory.bytesReserved() - memory.bytesAllocated(), 7); + EXPECT_EQ(memory.capacity() - memory.size(), 7); EXPECT_EQ(memory.allocate(sizeof(char), alignof(char)), reinterpret_cast(16)); - EXPECT_EQ(memory.bytesReserved() - memory.bytesAllocated(), + EXPECT_EQ(memory.capacity() - memory.size(), 6); // plenty left for an int, but not aligned EXPECT_THROW((void)memory.allocate(sizeof(int), alignof(int)), std::bad_alloc); } @@ -55,15 +57,15 @@ TEST(Allocate, Array) { // byte can be placed anywhere EXPECT_EQ(memory.allocate(sizeof(char) * 3, alignof(char)), reinterpret_cast(0)); - EXPECT_EQ(memory.bytesAllocated(), 3); + EXPECT_EQ(memory.size(), 3); // 2 ints after the 3rd byte must have 3 bytes padding, placed at 4 and taking 8 EXPECT_EQ(memory.allocate(sizeof(int) * 2, alignof(int)), reinterpret_cast(4)); - EXPECT_EQ(memory.bytesAllocated(), 12); + EXPECT_EQ(memory.size(), 12); // 2 doubles after 12 bytes must have 4 bytes padding, placed at 16, taking 16 more EXPECT_EQ(memory.allocate(sizeof(double) * 2, alignof(double)), reinterpret_cast(16)); - EXPECT_EQ(memory.bytesAllocated(), 32); + EXPECT_EQ(memory.size(), 32); } TEST(Allocate, Initialize) { @@ -97,13 +99,13 @@ TEST(Allocate, Initialize) { // Relaxed test case for MSVC where the debug vector allocates extra crap TEST(Allocate, VectorRelaxed) { linear_memory_resource alloc(100); - EXPECT_EQ(alloc.bytesAllocated(), 0); - EXPECT_EQ(alloc.bytesReserved(), 100); + EXPECT_EQ(alloc.size(), 0); + EXPECT_EQ(alloc.capacity(), 100); std::vector> vec(10, alloc); - EXPECT_GE(alloc.bytesAllocated(), 10); - auto allocated = alloc.bytesAllocated(); + EXPECT_GE(alloc.size(), 10); + auto allocated = alloc.size(); vec.reserve(20); - EXPECT_GT(alloc.bytesAllocated(), allocated); + EXPECT_GT(alloc.size(), allocated); EXPECT_THROW(vec.reserve(100), std::bad_alloc); } @@ -125,37 +127,37 @@ TEST(Allocate, Vector) { } linear_memory_resource alloc(30); - EXPECT_EQ(alloc.bytesAllocated(), 0); - EXPECT_EQ(alloc.bytesReserved(), 30); + EXPECT_EQ(alloc.size(), 0); + EXPECT_EQ(alloc.capacity(), 30); // Yes, this is possible but don't do it. std::vector can easily reallocate // which will leave unused holes in the linear allocator. std::vector> vec(10, alloc); - EXPECT_EQ(alloc.bytesAllocated(), 10); + EXPECT_EQ(alloc.size(), 10); vec.reserve(20); - EXPECT_EQ(alloc.bytesAllocated(), 30); + EXPECT_EQ(alloc.size(), 30); EXPECT_THROW(vec.reserve(21), std::bad_alloc); } TEST(Allocate, PmrAllocator) { pmr_linear_memory_resource> res(100); - EXPECT_EQ(res.bytesAllocated(), 0); - EXPECT_EQ(res.bytesReserved(), 100); + EXPECT_EQ(res.size(), 0); + EXPECT_EQ(res.capacity(), 100); std::pmr::polymorphic_allocator alloc(&res); std::span bytes = create::array(alloc, 10); EXPECT_EQ(bytes.size(), 10); - EXPECT_EQ(res.bytesAllocated(), 10); + EXPECT_EQ(res.size(), 10); } TEST(Allocate, PmrVectorRelaxed) { pmr_linear_memory_resource> res(100); - EXPECT_EQ(res.bytesAllocated(), 0); - EXPECT_EQ(res.bytesReserved(), 100); + EXPECT_EQ(res.size(), 0); + EXPECT_EQ(res.capacity(), 100); std::pmr::vector vec(10, &res); - EXPECT_GE(res.bytesAllocated(), 10); - auto allocated = res.bytesAllocated(); + EXPECT_GE(res.size(), 10); + auto allocated = res.size(); vec.reserve(20); - EXPECT_GT(res.bytesAllocated(), allocated); + EXPECT_GT(res.size(), allocated); EXPECT_THROW(vec.reserve(100), std::bad_alloc); } @@ -168,17 +170,17 @@ TEST(Allocate, Readme) { std::span array = decodeless::create::array(memory, {(int)1, 3, 6, 10, 15}); EXPECT_EQ(array.size(), 5); EXPECT_EQ(array[4], 15); - EXPECT_EQ(memory.bytesAllocated(), sizeof(int) * 5); + EXPECT_EQ(memory.size(), sizeof(int) * 5); double* alignedDouble = decodeless::create::object(memory, 42.0); EXPECT_EQ(*alignedDouble, 42.0); - EXPECT_EQ(memory.bytesAllocated(), sizeof(int) * 5 + sizeof(double) + 4); + EXPECT_EQ(memory.size(), sizeof(int) * 5 + sizeof(double) + 4); decodeless::pmr_linear_memory_resource res(100); std::pmr::polymorphic_allocator alloc(&res); // interface abstraction std::span bytes = decodeless::create::array(alloc, 10); EXPECT_EQ(bytes.size(), 10); - EXPECT_EQ(res.bytesAllocated(), 10); + EXPECT_EQ(res.size(), 10); } TEST(Allocate, References) { @@ -195,7 +197,7 @@ TEST(Allocate, References) { { [[maybe_unused]] std::span r = create::array(alloc_c, 1); } { [[maybe_unused]] std::span r = create::array(alloc_r, 1); } { [[maybe_unused]] std::span r = create::array(alloc_cr, 1); } - EXPECT_EQ(res.bytesAllocated(), sizeof(int) * 6); + EXPECT_EQ(res.size(), sizeof(int) * 6); std::vector init{42}; { [[maybe_unused]] std::span r = create::array(res, init); } @@ -204,5 +206,5 @@ TEST(Allocate, References) { { [[maybe_unused]] std::span r = create::array(alloc_c, init); } { [[maybe_unused]] std::span r = create::array(alloc_r, init); } { [[maybe_unused]] std::span r = create::array(alloc_cr, init); } - EXPECT_EQ(res.bytesAllocated(), sizeof(int) * 12); + EXPECT_EQ(res.size(), sizeof(int) * 12); } From 2820da753426549e09ac4f4cdaf1f9b8712a9ae3 Mon Sep 17 00:00:00 2001 From: pknowles Date: Sat, 1 Jun 2024 16:03:20 -0700 Subject: [PATCH 2/2] new tests and fix state after bad_alloc disclaimer: tests written with gpt-4 --- include/decodeless/allocator.hpp | 8 ++- test/src/allocator.cpp | 101 +++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/include/decodeless/allocator.hpp b/include/decodeless/allocator.hpp index a212638..3f85ed3 100644 --- a/include/decodeless/allocator.hpp +++ b/include/decodeless/allocator.hpp @@ -76,10 +76,10 @@ class linear_memory_resource { uintptr_t result = m_next + ((-static_cast(m_next)) & (align - 1)); // Allocate - m_next = result + bytes; + uintptr_t newNext = result + bytes; // Check for overflow and attempt to reallocate if possible - if (m_next > m_end) { + if (newNext > m_end) { if constexpr (realloc_resource_or_allocator) { // Allocate the larger of double the existing arena or enough to // fit what was just requested. @@ -104,6 +104,10 @@ class linear_memory_resource { throw std::bad_alloc(); } } + + // Safe to update m_next as no exceptions were thrown. + m_next = newNext; + return reinterpret_cast(result); } diff --git a/test/src/allocator.cpp b/test/src/allocator.cpp index b0aeccb..e68cd4f 100644 --- a/test/src/allocator.cpp +++ b/test/src/allocator.cpp @@ -208,3 +208,104 @@ TEST(Allocate, References) { { [[maybe_unused]] std::span r = create::array(alloc_cr, init); } EXPECT_EQ(res.size(), sizeof(int) * 12); } + +// Test allocation of zero bytes to ensure it handles no-operation requests +// correctly +TEST(Allocate, ZeroBytes) { + linear_memory_resource memory(23); + EXPECT_EQ(memory.allocate(0, 1), nullptr); + EXPECT_EQ(memory.size(), 0); +} + +// Test allocation that exactly matches the remaining capacity to verify +// boundary conditions +TEST(Allocate, ExactCapacity) { + linear_memory_resource memory(23); + EXPECT_EQ(memory.allocate(23, 1), reinterpret_cast(0)); + EXPECT_EQ(memory.size(), 23); +} + +// Test repeated allocations that gradually use up the memory to ensure +// consistent behavior as memory fills +TEST(Allocate, RepeatedAllocations) { + linear_memory_resource memory(23); + for (size_t i = 0; i < 23; i++) { + EXPECT_EQ(memory.allocate(1, 1), reinterpret_cast(i)); + EXPECT_EQ(memory.size(), i + 1); + } +} + +// Verify behavior when attempting to allocate more memory than available, +// beyond just expecting std::bad_alloc +TEST(Allocate, OutOfMemory) { + linear_memory_resource memory(23); + EXPECT_THROW((void)memory.allocate(24, 1), std::bad_alloc); + EXPECT_EQ(memory.size(), 0); +} + +// Test the allocator's response to unusual or extreme alignment requirements +TEST(Allocate, UnusualAlignment) { + linear_memory_resource memory(23); + EXPECT_EQ(memory.allocate(sizeof(int), 16), reinterpret_cast(0)); + EXPECT_EQ(memory.size(), sizeof(int)); +} + +// Test allocation of a large object to verify handling of large allocations +TEST(Allocate, LargeAllocation) { + linear_memory_resource memory(200'000'000); + EXPECT_EQ(memory.allocate(123'456'789, 1), reinterpret_cast(0)); + EXPECT_EQ(memory.size(), 123'456'789); +} + +// More detailed tests focusing on alignment +TEST(Allocate, Alignment) { + linear_memory_resource memory(1024); + + // Test alignment for different types + EXPECT_EQ(memory.allocate(sizeof(char), alignof(char)), reinterpret_cast(0)); + EXPECT_EQ(memory.size(), sizeof(char)); + + // Since char typically has an alignment of 1 and size of 1, the next int (usually 4 bytes + // alignment) should start at 4 + EXPECT_EQ(memory.allocate(sizeof(int), alignof(int)), reinterpret_cast(4)); + EXPECT_EQ(memory.size(), 4 + sizeof(int)); + + // Double typically requires alignment of 8, so it should start at the next multiple of 8 after + // the last int + EXPECT_EQ(memory.allocate(sizeof(double), alignof(double)), reinterpret_cast(8)); + EXPECT_EQ(memory.size(), 8 + sizeof(double)); + + // Long long typically requires alignment of 8 and should follow the double + EXPECT_EQ(memory.allocate(sizeof(long long), alignof(long long)), reinterpret_cast(16)); + EXPECT_EQ(memory.size(), 16 + sizeof(long long)); + + // Test alignment for arrays + EXPECT_EQ(memory.allocate(sizeof(char) * 3, alignof(char)), reinterpret_cast(24)); + EXPECT_EQ(memory.size(), 24 + sizeof(char) * 3); + + EXPECT_EQ(memory.allocate(sizeof(int) * 2, alignof(int)), reinterpret_cast(28)); + EXPECT_EQ(memory.size(), 28 + sizeof(int) * 2); + + EXPECT_EQ(memory.allocate(sizeof(double) * 2, alignof(double)), reinterpret_cast(40)); + EXPECT_EQ(memory.size(), 40 + sizeof(double) * 2); + +#if defined(_MSC_VER) + #pragma warning(push) + #pragma warning(disable : 4324) // "structure was padded due to alignment specifier" +#endif + + // Test alignment for objects with non-standard alignment + struct AlignedStruct { + int data; + alignas(16) int alignedData; + }; + +#if defined(_MSC_VER) + #pragma warning(pop) +#endif + + // AlignedStruct requires alignment of 16, so it should start at the next multiple of 16 + EXPECT_EQ(memory.allocate(sizeof(AlignedStruct), alignof(AlignedStruct)), + reinterpret_cast(64)); + EXPECT_EQ(memory.size(), 64 + sizeof(AlignedStruct)); +}