allocator tests

This commit is contained in:
cdemeyer-teachx
2025-09-12 12:01:39 +09:00
parent de3638c8ad
commit 14aa93ada0
4 changed files with 375 additions and 229 deletions

View File

@@ -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<char*>(ptr) + sizeof(void*) +
(alignment - (reinterpret_cast<uintptr_t>(static_cast<char*>(ptr) + sizeof(void*)) % alignment)) % alignment;
// Store original pointer for free
*(static_cast<void**>(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<void**>(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<uint8_t> 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<char*>(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<char*>(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<char*>(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<char*>(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<size_t> 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<MemoryPool> m_pools;
std::vector<Block> 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<pointer>(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<pointer>(m_allocator->allocate(size));
}
void deallocate(pointer ptr, size_type) {

View File

@@ -9,6 +9,7 @@ FetchContent_MakeAvailable(googletest)
set(TEST_SOURCES
test_main.cpp
test_allocator.cpp
)
# Create test executable

316
tests/test_allocator.cpp Normal file
View File

@@ -0,0 +1,316 @@
#include <gtest/gtest.h>
#include <vector>
#include <memory>
#include <span>
#include <cstring>
#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<uintptr_t>(ptr1) % 8, 0);
EXPECT_EQ(reinterpret_cast<uintptr_t>(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<uintptr_t>(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<void*> 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<uintptr_t>(ptr) % 8, 0);
}
}
// Test constructor with user-provided memory
TEST_F(WFCStackAllocatorTest, UserProvidedMemory) {
const size_t bufferSize = 512;
std::vector<uint8_t> buffer(bufferSize);
std::span<uint8_t> 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<uint8_t*>(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<int> 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<uintptr_t>(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<int, WFC::WFCStackAllocatorAdapter<int>>;
{
auto frame = allocator.createFrame();
IntVector vec((WFC::WFCStackAllocatorAdapter<int>(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<uintptr_t>(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

View File

@@ -2,8 +2,11 @@
#include <array>
#include <vector>
#include <algorithm>
#include <memory>
#include <span>
#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);