diff --git a/CMakeLists.txt b/CMakeLists.txt index ad19d6f..02c97a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,9 +22,24 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(toml11) +FetchContent_Declare( + doctest + GIT_REPOSITORY https://github.com/doctest/doctest.git + GIT_TAG 1da23a3e8119ec5cce4f9388e91b065e20bf06f5 # v2.4.12 +) +FetchContent_MakeAvailable(doctest) + include_directories(src) set(SOURCES src/main.cpp src/config.cpp) add_executable(weaseldb ${SOURCES}) target_link_libraries(weaseldb Threads::Threads toml11::toml11) + +enable_testing() + +add_executable(test_arena_allocator tests/test_arena_allocator.cpp) +target_link_libraries(test_arena_allocator doctest::doctest) +target_include_directories(test_arena_allocator PRIVATE src) + +add_test(NAME arena_allocator_tests COMMAND test_arena_allocator) diff --git a/src/arena_allocator.hpp b/src/arena_allocator.hpp new file mode 100644 index 0000000..bbec464 --- /dev/null +++ b/src/arena_allocator.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include + +class ArenaAllocator { +public: + explicit ArenaAllocator(size_t initial_size = 1024) + : block_size_(initial_size), current_block_(0), current_offset_(0) { + add_block(); + } + + ~ArenaAllocator() = default; + + ArenaAllocator(const ArenaAllocator &) = delete; + ArenaAllocator &operator=(const ArenaAllocator &) = delete; + + ArenaAllocator(ArenaAllocator &&) = default; + ArenaAllocator &operator=(ArenaAllocator &&) = default; + + void *allocate(size_t size, size_t alignment = alignof(std::max_align_t)) { + if (size == 0) { + return nullptr; + } + + char *block_start = blocks_[current_block_].get(); + uintptr_t block_addr = reinterpret_cast(block_start); + size_t aligned_offset = + align_up(block_addr + current_offset_, alignment) - block_addr; + + if (aligned_offset + size > block_size_) { + if (size > block_size_) { + throw std::bad_alloc(); + } + add_block(); + block_start = blocks_[current_block_].get(); + block_addr = reinterpret_cast(block_start); + aligned_offset = align_up(block_addr, alignment) - block_addr; + } + + void *ptr = block_start + aligned_offset; + current_offset_ = aligned_offset + size; + + return ptr; + } + + template T *construct(Args &&...args) { + void *ptr = allocate(sizeof(T), alignof(T)); + return new (ptr) T(std::forward(args)...); + } + + void reset() { + current_block_ = 0; + current_offset_ = 0; + } + + size_t total_allocated() const { return blocks_.size() * block_size_; } + + size_t used_bytes() const { + return current_block_ * block_size_ + current_offset_; + } + + size_t available_in_current_block() const { + return block_size_ - current_offset_; + } + + size_t num_blocks() const { return blocks_.size(); } + +private: + void add_block() { + blocks_.emplace_back(std::make_unique(block_size_)); + current_block_ = blocks_.size() - 1; + current_offset_ = 0; + } + + static size_t align_up(size_t value, size_t alignment) { + if (alignment == 0 || (alignment & (alignment - 1)) != 0) { + return value; + } + return (value + alignment - 1) & ~(alignment - 1); + } + + size_t block_size_; + size_t current_block_; + size_t current_offset_; + std::vector> blocks_; +}; diff --git a/tests/test_arena_allocator.cpp b/tests/test_arena_allocator.cpp new file mode 100644 index 0000000..e1c8008 --- /dev/null +++ b/tests/test_arena_allocator.cpp @@ -0,0 +1,277 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "arena_allocator.hpp" +#include +#include +#include + +TEST_CASE("ArenaAllocator basic construction") { + ArenaAllocator arena; + CHECK(arena.num_blocks() == 1); + CHECK(arena.used_bytes() == 0); + CHECK(arena.total_allocated() == 1024); + CHECK(arena.available_in_current_block() == 1024); +} + +TEST_CASE("ArenaAllocator custom initial size") { + ArenaAllocator arena(2048); + CHECK(arena.num_blocks() == 1); + CHECK(arena.total_allocated() == 2048); + CHECK(arena.available_in_current_block() == 2048); +} + +TEST_CASE("ArenaAllocator basic allocation") { + ArenaAllocator arena; + + SUBCASE("allocate zero bytes returns nullptr") { + void *ptr = arena.allocate(0); + CHECK(ptr == nullptr); + CHECK(arena.used_bytes() == 0); + } + + SUBCASE("allocate single byte") { + void *ptr = arena.allocate(1); + CHECK(ptr != nullptr); + CHECK(arena.used_bytes() >= 1); + } + + SUBCASE("allocate multiple bytes") { + void *ptr1 = arena.allocate(100); + void *ptr2 = arena.allocate(200); + + CHECK(ptr1 != nullptr); + CHECK(ptr2 != nullptr); + CHECK(ptr1 != ptr2); + CHECK(arena.used_bytes() >= 300); + } +} + +TEST_CASE("ArenaAllocator alignment") { + ArenaAllocator arena; + + SUBCASE("default alignment") { + void *ptr = arena.allocate(1); + CHECK(reinterpret_cast(ptr) % alignof(std::max_align_t) == 0); + } + + SUBCASE("custom alignment") { + void *ptr8 = arena.allocate(1, 8); + CHECK(reinterpret_cast(ptr8) % 8 == 0); + + void *ptr16 = arena.allocate(1, 16); + CHECK(reinterpret_cast(ptr16) % 16 == 0); + + void *ptr32 = arena.allocate(1, 32); + CHECK(reinterpret_cast(ptr32) % 32 == 0); + } + + SUBCASE("alignment with larger allocations") { + ArenaAllocator fresh_arena; + void *ptr = fresh_arena.allocate(100, 64); + CHECK(reinterpret_cast(ptr) % 64 == 0); + } +} + +TEST_CASE("ArenaAllocator block management") { + ArenaAllocator arena(128); + + SUBCASE("single block allocation") { + void *ptr = arena.allocate(64); + CHECK(ptr != nullptr); + CHECK(arena.num_blocks() == 1); + CHECK(arena.used_bytes() == 64); + } + + SUBCASE("multiple blocks when size exceeded") { + void *ptr1 = arena.allocate(100); + CHECK(arena.num_blocks() == 1); + + void *ptr2 = arena.allocate(50); + CHECK(arena.num_blocks() == 2); + CHECK(ptr1 != ptr2); + } + + SUBCASE("allocation larger than block size throws") { + CHECK_THROWS_AS(arena.allocate(200), std::bad_alloc); + } +} + +TEST_CASE("ArenaAllocator construct template") { + ArenaAllocator arena; + + SUBCASE("construct int") { + int *ptr = arena.construct(42); + CHECK(ptr != nullptr); + CHECK(*ptr == 42); + } + + SUBCASE("construct string") { + std::string *ptr = arena.construct("hello world"); + CHECK(ptr != nullptr); + CHECK(*ptr == "hello world"); + } + + SUBCASE("construct multiple objects") { + int *ptr1 = arena.construct(10); + int *ptr2 = arena.construct(20); + + CHECK(ptr1 != ptr2); + CHECK(*ptr1 == 10); + CHECK(*ptr2 == 20); + } + + SUBCASE("construct with multiple arguments") { + auto *ptr = arena.construct>(42, "test"); + CHECK(ptr != nullptr); + CHECK(ptr->first == 42); + CHECK(ptr->second == "test"); + } +} + +TEST_CASE("ArenaAllocator reset functionality") { + ArenaAllocator arena; + + arena.allocate(100); + arena.allocate(200); + size_t used_before = arena.used_bytes(); + CHECK(used_before > 0); + + arena.reset(); + CHECK(arena.used_bytes() == 0); + CHECK(arena.num_blocks() >= 1); + + void *ptr = arena.allocate(50); + CHECK(ptr != nullptr); + CHECK(arena.used_bytes() == 50); +} + +TEST_CASE("ArenaAllocator memory tracking") { + ArenaAllocator arena(512); + + CHECK(arena.total_allocated() == 512); + CHECK(arena.used_bytes() == 0); + CHECK(arena.available_in_current_block() == 512); + + arena.allocate(100); + CHECK(arena.used_bytes() >= 100); + CHECK(arena.available_in_current_block() <= 412); + + arena.allocate(400); + CHECK(arena.num_blocks() == 1); + + arena.allocate(50); + CHECK(arena.num_blocks() == 2); + CHECK(arena.total_allocated() == 1024); +} + +TEST_CASE("ArenaAllocator stress test") { + ArenaAllocator arena(1024); + + SUBCASE("many small allocations") { + std::vector ptrs; + for (int i = 0; i < 1000; ++i) { + void *ptr = arena.allocate(8); + CHECK(ptr != nullptr); + ptrs.push_back(ptr); + } + + for (size_t i = 1; i < ptrs.size(); ++i) { + CHECK(ptrs[i] != ptrs[i - 1]); + } + } + + SUBCASE("alternating small and large allocations") { + for (int i = 0; i < 50; ++i) { + void *small_ptr = arena.allocate(16); + void *large_ptr = arena.allocate(256); + CHECK(small_ptr != nullptr); + CHECK(large_ptr != nullptr); + CHECK(small_ptr != large_ptr); + } + } +} + +TEST_CASE("ArenaAllocator move semantics") { + ArenaAllocator arena1(512); + arena1.allocate(100); + size_t used_bytes = arena1.used_bytes(); + size_t num_blocks = arena1.num_blocks(); + + ArenaAllocator arena2 = std::move(arena1); + CHECK(arena2.used_bytes() == used_bytes); + CHECK(arena2.num_blocks() == num_blocks); + + void *ptr = arena2.allocate(50); + CHECK(ptr != nullptr); +} + +TEST_CASE("ArenaAllocator edge cases") { + SUBCASE("very small block size") { + ArenaAllocator arena(16); + void *ptr = arena.allocate(8); + CHECK(ptr != nullptr); + CHECK(arena.num_blocks() == 1); + } + + SUBCASE("allocation exactly block size") { + ArenaAllocator arena(64); + void *ptr = arena.allocate(64); + CHECK(ptr != nullptr); + CHECK(arena.num_blocks() == 1); + + void *ptr2 = arena.allocate(1); + CHECK(ptr2 != nullptr); + CHECK(arena.num_blocks() == 2); + } + + SUBCASE("multiple resets") { + ArenaAllocator arena; + for (int i = 0; i < 10; ++i) { + arena.allocate(100); + arena.reset(); + CHECK(arena.used_bytes() == 0); + } + } +} + +struct TestObject { + int value; + std::string name; + + TestObject(int v, const std::string &n) : value(v), name(n) {} + ~TestObject() = default; +}; + +TEST_CASE("ArenaAllocator with custom objects") { + ArenaAllocator arena; + + TestObject *obj1 = arena.construct(42, "first"); + TestObject *obj2 = arena.construct(84, "second"); + + CHECK(obj1 != nullptr); + CHECK(obj2 != nullptr); + CHECK(obj1 != obj2); + CHECK(obj1->value == 42); + CHECK(obj1->name == "first"); + CHECK(obj2->value == 84); + CHECK(obj2->name == "second"); +} + +TEST_CASE("ArenaAllocator alignment edge cases") { + ArenaAllocator arena; + + SUBCASE("unaligned then aligned allocation") { + void *ptr1 = arena.allocate(1, 1); + void *ptr2 = arena.allocate(8, 8); + + CHECK(ptr1 != nullptr); + CHECK(ptr2 != nullptr); + CHECK(reinterpret_cast(ptr2) % 8 == 0); + } + + SUBCASE("large alignment requirements") { + ArenaAllocator fresh_arena; + void *ptr = fresh_arena.allocate(1, 128); + CHECK(ptr != nullptr); + CHECK(reinterpret_cast(ptr) % 128 == 0); + } +}