diff --git a/include/nd-wfc/wfc_allocator.hpp b/include/nd-wfc/wfc_allocator.hpp index c12d081..3bb6bed 100644 --- a/include/nd-wfc/wfc_allocator.hpp +++ b/include/nd-wfc/wfc_allocator.hpp @@ -12,75 +12,8 @@ #include "wfc_utils.hpp" -#define WFC_USE_STACK_ALLOCATOR - -inline void* allocate_aligned_memory(size_t alignment, size_t size) { -#ifdef WFC_USE_STACK_ALLOCATOR - void* ptr = nullptr; - -#ifdef _MSC_VER - ptr = _aligned_malloc(size, alignment); -#elif defined(__GNUC__) || defined(__clang__) -#if __cplusplus >= 201703L - ptr = std::aligned_alloc(alignment, size); -#else -#ifdef POSIX_MEMALIGN - posix_memalign(&ptr, alignment, size); -#endif -#endif -#else -#if __cplusplus >= 201703L - ptr = std::aligned_alloc(alignment, size); -#endif -#endif - - return ptr; -#else - // When not using stack allocator, use standard malloc with manual alignment - void* ptr = std::malloc(size + alignment - 1 + sizeof(void*)); - if (!ptr) return nullptr; - - void* aligned_ptr = static_cast(ptr) + sizeof(void*) + - (alignment - (reinterpret_cast(static_cast(ptr) + sizeof(void*)) % alignment)) % alignment; - - // Store original pointer for free - *(static_cast(aligned_ptr) - 1) = ptr; - - return aligned_ptr; -#endif -} - -inline void free_aligned_memory(void* ptr) { - if (!ptr) return; - -#ifdef WFC_USE_STACK_ALLOCATOR -#ifdef _MSC_VER - _aligned_free(ptr); -#elif defined(__GNUC__) || defined(__clang__) -#if __cplusplus >= 201703L - std::free(ptr); -#else -#ifdef POSIX_MEMALIGN - free(ptr); -#endif -#endif -#else -#if __cplusplus >= 201703L - std::free(ptr); -#else - free(ptr); -#endif -#endif -#else - // When not using stack allocator, free the original pointer - void* original_ptr = *(static_cast(ptr) - 1); - std::free(original_ptr); -#endif -} - namespace WFC { -#ifdef WFC_USE_STACK_ALLOCATOR /** * @brief Stack allocator specifically designed for WFC branching operations * @@ -99,16 +32,6 @@ private: MemoryPool(void* p, size_t s) : ptr(p), size(s), used(0) {} }; - struct Block { - void* ptr; - size_t size; - size_t alignment; - size_t poolIndex; // Which pool this allocation came from - - Block() : ptr(nullptr), size(0), alignment(0), poolIndex(0) {} - Block(void* p, size_t s, size_t a, size_t pi) : ptr(p), size(s), alignment(a), poolIndex(pi) {} - }; - public: /** * @brief Construct allocator with initial capacity @@ -116,14 +39,22 @@ public: */ explicit WFCStackAllocator(size_t initialCapacity = 1024 * 1024) // 1MB default { + addPool(0); // first pool is 0 addPool(initialCapacity); + m_currentPoolIndex = 1; + } + + explicit WFCStackAllocator(std::span userGivenData) + { + m_pools.push_back(MemoryPool(userGivenData.data(), userGivenData.size())); + m_currentPoolIndex = 0; } ~WFCStackAllocator() { - for (auto& pool : m_pools) { - if (pool.ptr) { - free_aligned_memory(pool.ptr); - } + // first pool is not deallocated because it's either empty or comes from the user + for (size_t i = 1; i < m_pools.size(); ++i) + { + delete[] static_cast(m_pools[i].ptr); } } @@ -133,55 +64,31 @@ public: WFCStackAllocator(WFCStackAllocator&&) = delete; WFCStackAllocator& operator=(WFCStackAllocator&&) = delete; - /** - * @brief Allocate memory from the stack - * @param size Number of bytes to allocate - * @param alignment Memory alignment requirement (default 8) - * @return Pointer to allocated memory - */ - void* allocate(size_t size, size_t alignment = 8) { - // Try to allocate from existing pools - for (size_t i = 0; i < m_pools.size(); ++i) { - auto& pool = m_pools[i]; - - // Align the current position in this pool - size_t alignedUsed = alignUp(pool.used, alignment); - - // Check if we have enough space in this pool - if (alignedUsed + size <= pool.size) { - void* ptr = static_cast(pool.ptr) + alignedUsed; - pool.used = alignedUsed + size; - - // Track this allocation for proper cleanup - m_allocations.emplace_back(ptr, size, alignment, i); + void* allocate(size_t size) + { + size = alignUp(size); + for (; m_currentPoolIndex < m_pools.size(); ++m_currentPoolIndex) + { + if (getCapacity() >= size) + { + auto& pool = m_pools[m_currentPoolIndex]; + void* ptr = static_cast(pool.ptr) + pool.used; + pool.used += size; return ptr; } } + // No existing pool has enough space, add a new one size_t newPoolSize = std::max(m_pools.back().size * 2, size * 2); // Grow exponentially addPool(newPoolSize); - // Now allocate from the new pool (which is the last one) - auto& newPool = m_pools.back(); - void* ptr = static_cast(newPool.ptr); - newPool.used = size; - - // Track this allocation for proper cleanup - m_allocations.emplace_back(ptr, size, alignment, m_pools.size() - 1); - - return ptr; + return allocate(size); } - /** - * @brief Deallocate memory (no-op as requested - memory freed when branch goes out of scope) - * @param ptr Pointer to deallocate - */ - void deallocate(void*) { - // No-op as requested - deallocation happens when branch goes out of scope - // The stack nature ensures automatic cleanup - } + void deallocate(void*) + {} /** * @brief Create a stack frame marker for RAII-based cleanup @@ -190,40 +97,25 @@ public: class StackFrame { private: WFCStackAllocator& m_allocator; - std::vector m_savedUsed; - size_t m_savedAllocCount; + size_t m_poolIndex{}; + size_t m_poolUsed{}; public: StackFrame(WFCStackAllocator& allocator) : m_allocator(allocator) - , m_savedAllocCount(allocator.m_allocations.size()) + , m_poolIndex(allocator.m_currentPoolIndex) + , m_poolUsed(allocator.m_pools[m_poolIndex].used) + {} + + ~StackFrame() { - // Save the current used state of all pools - m_savedUsed.reserve(allocator.m_pools.size()); - for (const auto& pool : allocator.m_pools) { - m_savedUsed.push_back(pool.used); - } - } - - ~StackFrame() { - // Restore the used state of all pools - for (size_t i = 0; i < m_savedUsed.size() && i < m_allocator.m_pools.size(); ++i) { - m_allocator.m_pools[i].used = m_savedUsed[i]; + for (size_t i = m_allocator.m_pools.size() - 1; i > m_poolIndex; --i) + { + m_allocator.m_pools[i].used = 0; } - // Remove any new pools that were added during this frame - if (m_allocator.m_pools.size() > m_savedUsed.size()) { - // Free the additional pools that were added - for (size_t i = m_savedUsed.size(); i < m_allocator.m_pools.size(); ++i) { - if (m_allocator.m_pools[i].ptr) { - free_aligned_memory(m_allocator.m_pools[i].ptr); - } - } - m_allocator.m_pools.resize(m_savedUsed.size()); - } - - // Remove allocations that were made during this frame - m_allocator.m_allocations.resize(m_savedAllocCount); + m_allocator.m_pools[m_poolIndex].used = m_poolUsed; + m_allocator.m_currentPoolIndex = m_poolIndex; } // Non-copyable, movable @@ -236,106 +128,36 @@ public: /** * @brief Create a new stack frame for a branch */ - StackFrame createFrame() { + StackFrame createFrame() + { return StackFrame(*this); } - /** - * @brief Get current memory usage - */ - size_t getUsed() const { - size_t total = 0; - for (const auto& pool : m_pools) { - total += pool.used; - } - return total; + constexpr size_t getCapacity() const + { + return m_pools[m_currentPoolIndex].size - m_pools[m_currentPoolIndex].used; } - /** - * @brief Get total capacity - */ - size_t getCapacity() const { - size_t total = 0; - for (const auto& pool : m_pools) { - total += pool.size; - } - return total; - } - - /** - * @brief Get allocation count - */ - size_t getAllocationCount() const { return m_allocations.size(); } - - /** - * @brief Reset the allocator (useful for reusing between WFC runs) - */ - void reset() { - for (auto& pool : m_pools) { - pool.used = 0; - } - m_allocations.clear(); + static constexpr size_t alignUp(size_t value) + { + return (value + 8 - 1) & ~(8 - 1); } private: - void addPool(size_t size) { - void* ptr = allocate_aligned_memory(64, size); // 64-byte alignment for cache efficiency + constexpr void addPool(size_t size) + { + void* ptr = new char[size]; // Allocate bytes if (!ptr) { throw std::bad_alloc(); } m_pools.emplace_back(ptr, size); } - size_t alignUp(size_t value, size_t alignment) const { - return (value + alignment - 1) & ~(alignment - 1); - } - private: std::vector m_pools; - std::vector m_allocations; + size_t m_currentPoolIndex{}; }; -#else // WFC_USE_STACK_ALLOCATOR not defined -/** - * @brief Simplified allocator using standard malloc/free - */ -class WFCStackAllocator { -public: - explicit WFCStackAllocator(size_t = 1024 * 1024) {} - ~WFCStackAllocator() = default; - - // Non-copyable, non-movable for consistency - WFCStackAllocator(const WFCStackAllocator&) = delete; - WFCStackAllocator& operator=(const WFCStackAllocator&) = delete; - WFCStackAllocator(WFCStackAllocator&&) = delete; - WFCStackAllocator& operator=(WFCStackAllocator&&) = delete; - - void* allocate(size_t size, size_t alignment = 8) { - return allocate_aligned_memory(alignment, size); - } - - void deallocate(void* ptr) { - free_aligned_memory(ptr); - } - - class StackFrame { - public: - StackFrame(WFCStackAllocator&) {} - ~StackFrame() = default; - StackFrame(const StackFrame&) = delete; - StackFrame& operator=(const StackFrame&) = delete; - StackFrame(StackFrame&&) = default; - StackFrame& operator=(StackFrame&&) = default; - }; - - StackFrame createFrame() { return StackFrame(*this); } - size_t getUsed() const { return 0; } - size_t getCapacity() const { return 0; } - size_t getAllocationCount() const { return 0; } - void reset() {} -}; -#endif // WFC_USE_STACK_ALLOCATOR - /** * @brief Custom allocator adapter for STL containers using WFCStackAllocator */ @@ -363,7 +185,11 @@ public: : m_allocator(other.m_allocator) {} pointer allocate(size_type n) { - return static_cast(m_allocator->allocate(n * sizeof(T), alignof(T))); + size_t size = n * sizeof(T); + size_t alignment = alignof(T); + // Ensure alignment + size = (size + alignment - 1) & ~(alignment - 1); + return static_cast(m_allocator->allocate(size)); } void deallocate(pointer ptr, size_type) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2a40345..a5d8c75 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,6 +9,7 @@ FetchContent_MakeAvailable(googletest) set(TEST_SOURCES test_main.cpp + test_allocator.cpp ) # Create test executable diff --git a/tests/test_allocator.cpp b/tests/test_allocator.cpp new file mode 100644 index 0000000..a049cea --- /dev/null +++ b/tests/test_allocator.cpp @@ -0,0 +1,316 @@ +#include +#include +#include +#include +#include +#include "nd-wfc/wfc_allocator.hpp" + +namespace { + +// Test fixture for WFCStackAllocator tests +class WFCStackAllocatorTest : public ::testing::Test { +protected: + void SetUp() override { + // Setup if needed + } + + void TearDown() override { + // Cleanup if needed + } +}; + +// Test basic allocation and deallocation +TEST_F(WFCStackAllocatorTest, BasicAllocation) { + WFC::WFCStackAllocator allocator(1024); + + void* ptr1 = allocator.allocate(64); + ASSERT_NE(ptr1, nullptr); + + void* ptr2 = allocator.allocate(128); + ASSERT_NE(ptr2, nullptr); + ASSERT_NE(ptr1, ptr2); // Should be different addresses + + // Check that allocations are properly aligned + EXPECT_EQ(reinterpret_cast(ptr1) % 8, 0); + EXPECT_EQ(reinterpret_cast(ptr2) % 8, 0); + + // deallocate doesn't do anything in this allocator + allocator.deallocate(ptr1); + allocator.deallocate(ptr2); +} + +// Test alignment +TEST_F(WFCStackAllocatorTest, Alignment) { + WFC::WFCStackAllocator allocator(1024); + + // Test various sizes and ensure 8-byte alignment + for (size_t size : {1, 3, 7, 9, 15, 17}) { + void* ptr = allocator.allocate(size); + ASSERT_NE(ptr, nullptr); + EXPECT_EQ(reinterpret_cast(ptr) % 8, 0); + } +} + +// Test stack frame functionality +TEST_F(WFCStackAllocatorTest, StackFrame) { + WFC::WFCStackAllocator allocator(1024); + + // Allocate some memory in the root frame + void* rootPtr = allocator.allocate(64); + ASSERT_NE(rootPtr, nullptr); + + size_t initialCapacity = allocator.getCapacity(); + + { + // Create a new stack frame + auto frame = allocator.createFrame(); + + // Allocate in the new frame + void* framePtr1 = allocator.allocate(32); + void* framePtr2 = allocator.allocate(48); + ASSERT_NE(framePtr1, nullptr); + ASSERT_NE(framePtr2, nullptr); + + // Capacity should be reduced + EXPECT_LT(allocator.getCapacity(), initialCapacity); + + // Frame goes out of scope, memory should be freed + } + + // After frame destruction, capacity should be restored + EXPECT_EQ(allocator.getCapacity(), initialCapacity); + + // We can still allocate (should reuse the freed space) + void* newPtr = allocator.allocate(32); + ASSERT_NE(newPtr, nullptr); +} + +// Test nested stack frames +TEST_F(WFCStackAllocatorTest, NestedStackFrames) { + WFC::WFCStackAllocator allocator(1024); + + void* rootPtr = allocator.allocate(32); + size_t rootCapacity = allocator.getCapacity(); + + { + auto frame1 = allocator.createFrame(); + void* frame1Ptr = allocator.allocate(32); + size_t frame1Capacity = allocator.getCapacity(); + + { + auto frame2 = allocator.createFrame(); + void* frame2Ptr = allocator.allocate(32); + size_t frame2Capacity = allocator.getCapacity(); + + // Each nested frame should have less capacity + EXPECT_LT(frame2Capacity, frame1Capacity); + EXPECT_LT(frame1Capacity, rootCapacity); + + // frame2 goes out of scope + } + + // Back to frame1 capacity + EXPECT_EQ(allocator.getCapacity(), frame1Capacity); + + // frame1 goes out of scope + } + + // Back to root capacity + EXPECT_EQ(allocator.getCapacity(), rootCapacity); +} + +// Test automatic pool expansion +TEST_F(WFCStackAllocatorTest, PoolExpansion) { + WFC::WFCStackAllocator allocator(128); // Small initial pool + + // Allocate until we exceed the initial pool + std::vector allocations; + size_t totalAllocated = 0; + + while (totalAllocated < 1000) { // More than initial capacity + void* ptr = allocator.allocate(64); + ASSERT_NE(ptr, nullptr); + allocations.push_back(ptr); + totalAllocated += 64; + } + + // All allocations should be valid and aligned + for (void* ptr : allocations) { + ASSERT_NE(ptr, nullptr); + EXPECT_EQ(reinterpret_cast(ptr) % 8, 0); + } +} + +// Test constructor with user-provided memory +TEST_F(WFCStackAllocatorTest, UserProvidedMemory) { + const size_t bufferSize = 512; + std::vector buffer(bufferSize); + + std::span span(buffer.data(), buffer.size()); + WFC::WFCStackAllocator allocator(span); + + // Should be able to allocate from the provided buffer + void* ptr1 = allocator.allocate(64); + ASSERT_NE(ptr1, nullptr); + + // Pointer should be within our buffer + EXPECT_GE(ptr1, buffer.data()); + EXPECT_LT(static_cast(ptr1) + 64, buffer.data() + bufferSize); + + void* ptr2 = allocator.allocate(128); + ASSERT_NE(ptr2, nullptr); + + // Should still be able to expand to new pools when user buffer is exhausted + void* ptr3 = allocator.allocate(bufferSize); // Larger than user buffer + ASSERT_NE(ptr3, nullptr); +} + +// Test allocator adapter +TEST_F(WFCStackAllocatorTest, AllocatorAdapter) { + WFC::WFCStackAllocator allocator(1024); + WFC::WFCStackAllocatorAdapter adapter(allocator); + + // Test allocation + int* ptr = adapter.allocate(10); // Space for 10 ints + ASSERT_NE(ptr, nullptr); + + // Should be properly aligned for int + EXPECT_EQ(reinterpret_cast(ptr) % alignof(int), 0); + + // Test deallocation + adapter.deallocate(ptr, 10); +} + +// Test STL container with custom allocator +TEST_F(WFCStackAllocatorTest, STLContainerWithAdapter) { + WFC::WFCStackAllocator allocator(1024); + using IntVector = std::vector>; + + { + auto frame = allocator.createFrame(); + IntVector vec((WFC::WFCStackAllocatorAdapter(allocator))); + + // Add some elements + for (int i = 0; i < 10; ++i) { + vec.push_back(i); + } + + EXPECT_EQ(vec.size(), 10); + for (int i = 0; i < 10; ++i) { + EXPECT_EQ(vec[i], i); + } + + // Vector goes out of scope, memory freed with frame + } + + // Should be able to allocate again (reusing freed memory) + void* newAlloc = allocator.allocate(64); + ASSERT_NE(newAlloc, nullptr); +} + +// Test alignment helper function +TEST_F(WFCStackAllocatorTest, AlignUp) { + EXPECT_EQ(WFC::WFCStackAllocator::alignUp(0), 0); + EXPECT_EQ(WFC::WFCStackAllocator::alignUp(1), 8); + EXPECT_EQ(WFC::WFCStackAllocator::alignUp(7), 8); + EXPECT_EQ(WFC::WFCStackAllocator::alignUp(8), 8); + EXPECT_EQ(WFC::WFCStackAllocator::alignUp(9), 16); + EXPECT_EQ(WFC::WFCStackAllocator::alignUp(15), 16); + EXPECT_EQ(WFC::WFCStackAllocator::alignUp(16), 16); +} + +// Test edge case: allocate zero bytes +TEST_F(WFCStackAllocatorTest, ZeroAllocation) { + WFC::WFCStackAllocator allocator(1024); + + void* ptr = allocator.allocate(0); + // Should return a valid pointer (can be null or non-null depending on implementation) + // But should not crash + + allocator.deallocate(ptr); +} + +// Test edge case: very large allocation +TEST_F(WFCStackAllocatorTest, LargeAllocation) { + WFC::WFCStackAllocator allocator(1024); + + // Allocate something larger than initial capacity + void* ptr = allocator.allocate(2000); + ASSERT_NE(ptr, nullptr); + EXPECT_EQ(reinterpret_cast(ptr) % 8, 0); +} + +// Test memory reuse after frame destruction +TEST_F(WFCStackAllocatorTest, MemoryReuse) { + WFC::WFCStackAllocator allocator(1024); + + size_t initialCapacity = allocator.getCapacity(); + + { + auto frame = allocator.createFrame(); + allocator.allocate(64); + allocator.allocate(64); + + // Should have less capacity during frame + EXPECT_LT(allocator.getCapacity(), initialCapacity); + } + + // After frame destruction, should be back to initial capacity + EXPECT_EQ(allocator.getCapacity(), initialCapacity); +} + +// Test multiple frames and complex nesting +TEST_F(WFCStackAllocatorTest, ComplexFrameNesting) { + WFC::WFCStackAllocator allocator(1024); + + // Root allocations + void* root1 = allocator.allocate(32); + void* root2 = allocator.allocate(32); + + { + auto frame1 = allocator.createFrame(); + void* f1_1 = allocator.allocate(32); + void* f1_2 = allocator.allocate(32); + + { + auto frame2 = allocator.createFrame(); + void* f2_1 = allocator.allocate(32); + + { + auto frame3 = allocator.createFrame(); + void* f3_1 = allocator.allocate(32); + // frame3 ends + } + + void* f2_2 = allocator.allocate(32); + // frame2 ends + } + + void* f1_3 = allocator.allocate(32); + // frame1 ends + } + + // All frame memory should be freed, root allocations still valid + void* root3 = allocator.allocate(32); // Should reuse freed space + ASSERT_NE(root3, nullptr); +} + +// Test that deallocation is a no-op +TEST_F(WFCStackAllocatorTest, DeallocateIsNoOp) { + WFC::WFCStackAllocator allocator(1024); + + void* ptr = allocator.allocate(64); + size_t capacityBefore = allocator.getCapacity(); + + // deallocate should do nothing + allocator.deallocate(ptr); + + // Capacity should be unchanged + EXPECT_EQ(allocator.getCapacity(), capacityBefore); + + // Should be able to allocate again + void* ptr2 = allocator.allocate(64); + ASSERT_NE(ptr2, nullptr); +} + +} // namespace diff --git a/tests/test_main.cpp b/tests/test_main.cpp index bece00e..be360dc 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -2,8 +2,11 @@ #include #include #include +#include +#include #include "nd-wfc/wfc.hpp" #include "nd-wfc/worlds.hpp" +#include "nd-wfc/wfc_allocator.hpp" int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv);