diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5be8c1f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,37 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# Top-most EditorConfig file +root = true + +# All files +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# C++ files +[*.{cpp,h,hpp,cc}] +indent_style = space +indent_size = 4 + +# CMake files +[{CMakeLists.txt,*.cmake}] +indent_style = space +indent_size = 4 + +# Markdown files +[*.md] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = false + +# YAML files +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +# JSON files +[*.json] +indent_style = space +indent_size = 2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..bc74c6e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,183 @@ +# Contributing to fast-alloc + +Thank you for your interest in contributing to fast-alloc! This document provides guidelines and information for contributors. + +## Code of Conduct + +- Be respectful and constructive in all interactions +- Focus on what is best for the community and the project +- Show empathy towards other community members + +## How to Contribute + +### Reporting Bugs + +If you find a bug, please create an issue with: +- A clear, descriptive title +- Steps to reproduce the issue +- Expected behavior vs actual behavior +- Your environment (OS, compiler, version) +- Minimal code example demonstrating the bug + +### Suggesting Enhancements + +Enhancement suggestions are welcome! Please include: +- Clear description of the enhancement +- Use cases and benefits +- Potential implementation approach (if you have ideas) +- Any trade-offs or considerations + +### Pull Requests + +1. **Fork and Branch**: Fork the repository and create a feature branch + ```bash + git checkout -b feature/your-feature-name + ``` + +2. **Code Style**: + - Follow existing code style and conventions + - Use C++20 features appropriately + - Keep functions focused and reasonably sized + - Add comments for complex algorithms + - Use descriptive variable names + +3. **Testing**: + - Add tests for new features + - Ensure all existing tests pass + - Test on multiple platforms if possible (Windows, Linux, macOS) + - Run with sanitizers to catch memory issues: + ```bash + # Address Sanitizer + cmake -B build -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" + cmake --build build + ctest --test-dir build --output-on-failure + + # Thread Sanitizer (for thread-safe allocators) + cmake -B build -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_FLAGS="-fsanitize=thread -g" + cmake --build build + TSAN_OPTIONS="second_deadlock_stack=1 suppressions=tsan.supp" \ + ctest --test-dir build --output-on-failure + ``` + +4. **Documentation**: + - Update README.md if adding new features + - Add Doxygen comments for public APIs + - Update docs/architecture.md for architectural changes + - Include usage examples for new allocators + +5. **Commit Messages**: + - Use clear, descriptive commit messages + - Start with a verb in imperative mood (e.g., "Add", "Fix", "Update") + - Keep the first line under 72 characters + - Add detailed explanation in the commit body if needed + +6. **Pull Request Process**: + - Ensure CI passes (all tests, sanitizers) + - Update documentation as needed + - Reference any related issues + - Wait for review and address feedback + +## Development Setup + +### Prerequisites + +- C++20 compatible compiler (GCC 10+, Clang 10+, MSVC 2019+) +- CMake 3.14 or higher +- Git + +### Building + +```bash +# Configure +cmake -B build -DCMAKE_BUILD_TYPE=Release + +# Build +cmake --build build --config Release + +# Run tests +ctest --test-dir build --output-on-failure + +# Run benchmarks +./build/alloc_benchmarks +``` + +### Project Structure + +``` +fast-alloc/ +├── src/ # Source code +│ ├── *_allocator.h/cpp # Allocator implementations +├── tests/ # Unit tests (Catch2) +├── benchmarks/ # Performance benchmarks (Google Benchmark) +├── docs/ # Documentation +└── .github/workflows/ # CI configuration +``` + +## Code Guidelines + +### Memory Safety + +- Always validate pointers in debug builds using assertions +- Use RAII for resource management +- Avoid raw pointer arithmetic where alternatives exist +- Test with sanitizers (ASan, TSan, UBSan) + +### Performance + +- Profile before optimizing +- Benchmark any performance-related changes +- Document performance characteristics +- Consider cache locality and alignment + +### Assertions + +Use assertions for preconditions that should never fail in correct usage: +```cpp +assert(size > 0 && "Size must be positive"); +assert(memory_ && "Allocator not initialized"); +``` + +### Thread Safety + +- Document thread-safety guarantees in headers +- Use appropriate synchronization primitives +- Test concurrent operations under ThreadSanitizer +- Consider lock-free alternatives for hot paths + +## Testing Guidelines + +### Unit Tests + +- Test one thing per test case +- Use descriptive test names +- Cover edge cases (nullptr, zero size, exhaustion) +- Test move semantics for movable types +- Verify alignment requirements + +### Benchmark Tests + +- Compare against standard allocation (malloc/new) +- Test realistic usage patterns +- Run multiple iterations to reduce variance +- Document platform and compiler used + +## Release Process + +Maintainers will: +1. Review and merge pull requests +2. Update version numbers +3. Create release notes +4. Tag releases + +## Questions? + +Feel free to: +- Open an issue for questions +- Start a discussion for general topics +- Contact the maintainer directly + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/README.md b/README.md index 891ea18..e6b510b 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,11 @@ fast-alloc/ ## Usage Examples -### Pool Allocator +For comprehensive usage examples and patterns, see [docs/USAGE.md](docs/USAGE.md). + +### Quick Start + +#### Pool Allocator ```cpp #include "pool_allocator.h" @@ -126,7 +130,7 @@ void* obj = pool.allocate(); pool.deallocate(obj); ``` -### Thread-Safe Pool Allocator +#### Thread-Safe Pool Allocator ```cpp #include "threadsafe_pool_allocator.h" @@ -146,7 +150,7 @@ t1.join(); t2.join(); ``` -### Stack Allocator +#### Stack Allocator ```cpp #include "stack_allocator.h" @@ -162,7 +166,7 @@ void frame_update() { } ``` -### Free List Allocator +#### Free List Allocator ```cpp #include "freelist_allocator.h" @@ -198,6 +202,10 @@ Comprehensive test suite with Catch2: - Concurrent stress tests for thread-safe variants - CI with multiple sanitizers (ASan, TSan, UBSan) +## Contributing + +Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + ## Licence MIT Licence - see [LICENSE](LICENSE) for details. diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..59b5e59 --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,480 @@ +# Usage Guide + +This guide provides comprehensive examples and best practices for using fast-alloc allocators. + +## Table of Contents + +- [Pool Allocator](#pool-allocator) +- [Thread-Safe Pool Allocator](#thread-safe-pool-allocator) +- [Stack Allocator](#stack-allocator) +- [Free List Allocator](#free-list-allocator) +- [Best Practices](#best-practices) +- [Common Patterns](#common-patterns) + +## Pool Allocator + +### Basic Usage + +```cpp +#include "pool_allocator.h" + +// Create pool for 64-byte blocks, 1000 capacity +fast_alloc::PoolAllocator pool(64, 1000); + +// Allocate a block +void* block = pool.allocate(); +if (block) { + // Use block... + pool.deallocate(block); +} +``` + +### Particle System Example + +```cpp +struct Particle { + float x, y, z; + float vx, vy, vz; + float lifetime; + // ... other properties +}; + +class ParticleSystem { + fast_alloc::PoolAllocator pool_; + +public: + ParticleSystem() : pool_(sizeof(Particle), 10000) {} + + Particle* spawn(float x, float y, float z) { + void* mem = pool_.allocate(); + if (!mem) return nullptr; // Pool exhausted + + return new (mem) Particle{x, y, z, 0, 0, 0, 1.0f}; + } + + void destroy(Particle* p) { + p->~Particle(); + pool_.deallocate(p); + } +}; +``` + +### Checking Pool Status + +```cpp +fast_alloc::PoolAllocator pool(64, 100); + +std::cout << "Block size: " << pool.block_size() << " bytes\n"; +std::cout << "Capacity: " << pool.capacity() << " blocks\n"; +std::cout << "Allocated: " << pool.allocated() << " blocks\n"; +std::cout << "Full: " << (pool.is_full() ? "yes" : "no") << "\n"; +``` + +## Thread-Safe Pool Allocator + +### Multi-threaded Audio System + +```cpp +#include "threadsafe_pool_allocator.h" +#include +#include + +struct AudioVoice { + float frequency; + float amplitude; + // ... other properties +}; + +class AudioEngine { + fast_alloc::ThreadSafePoolAllocator pool_; + +public: + AudioEngine() : pool_(sizeof(AudioVoice), 256) {} + + // Called from multiple threads + AudioVoice* allocate_voice() { + void* mem = pool_.allocate(); + if (!mem) return nullptr; + return new (mem) AudioVoice{}; + } + + void free_voice(AudioVoice* voice) { + voice->~AudioVoice(); + pool_.deallocate(voice); + } +}; + +void audio_thread(AudioEngine& engine) { + for (int i = 0; i < 100; ++i) { + AudioVoice* voice = engine.allocate_voice(); + if (voice) { + // Process audio... + engine.free_voice(voice); + } + } +} + +int main() { + AudioEngine engine; + + std::vector threads; + for (int i = 0; i < 4; ++i) { + threads.emplace_back(audio_thread, std::ref(engine)); + } + + for (auto& t : threads) { + t.join(); + } +} +``` + +## Stack Allocator + +### Frame-Based Allocation + +```cpp +#include "stack_allocator.h" + +class Game { + fast_alloc::StackAllocator frame_allocator_; + +public: + Game() : frame_allocator_(1024 * 1024) {} // 1MB per frame + + void update() { + // Allocate temporary data for this frame + void* temp_buffer = frame_allocator_.allocate(4096); + void* particle_buffer = frame_allocator_.allocate(8192); + + // Use buffers... + + // Reset at end of frame - O(1) operation! + frame_allocator_.reset(); + } +}; +``` + +### Scoped Allocations with Markers + +```cpp +fast_alloc::StackAllocator stack(1024 * 1024); + +void process_level() { + void* marker = stack.get_marker(); + + // Allocate level data + void* level_data = stack.allocate(100000); + void* entity_data = stack.allocate(50000); + + // Process level... + + // Rewind to marker when done + stack.reset(marker); +} + +void main_loop() { + for (int frame = 0; frame < 60; ++frame) { + process_level(); + // Each frame starts with clean slate + } +} +``` + +### Alignment Example + +```cpp +fast_alloc::StackAllocator stack(1024); + +// Allocate with specific alignment (e.g., for SIMD) +void* simd_buffer = stack.allocate(256, 32); // 32-byte aligned +void* cache_line = stack.allocate(64, 64); // 64-byte aligned + +// Verify alignment +assert(reinterpret_cast(simd_buffer) % 32 == 0); +``` + +### Checking Stack Status + +```cpp +fast_alloc::StackAllocator stack(1024); + +std::cout << "Capacity: " << stack.capacity() << " bytes\n"; +std::cout << "Used: " << stack.used() << " bytes\n"; +std::cout << "Available: " << stack.available() << " bytes\n"; + +void* data = stack.allocate(500); +std::cout << "After allocation: " << stack.used() << " bytes used\n"; +``` + +## Free List Allocator + +### Basic Variable-Size Allocation + +```cpp +#include "freelist_allocator.h" + +fast_alloc::FreeListAllocator allocator( + 1024 * 1024, // 1MB total + fast_alloc::FreeListStrategy::FirstFit +); + +// Allocate different sizes +void* small = allocator.allocate(100); +void* medium = allocator.allocate(1000); +void* large = allocator.allocate(10000); + +// Deallocate in any order +allocator.deallocate(medium); +allocator.deallocate(small); +allocator.deallocate(large); +``` + +### Asset Manager Example + +```cpp +class AssetManager { + fast_alloc::FreeListAllocator allocator_; + +public: + AssetManager() + : allocator_(100 * 1024 * 1024, // 100MB + fast_alloc::FreeListStrategy::BestFit) {} + + void* load_texture(size_t size) { + return allocator_.allocate(size, 16); // 16-byte aligned + } + + void* load_mesh(size_t size) { + return allocator_.allocate(size, 32); // 32-byte aligned + } + + void unload_asset(void* asset) { + allocator_.deallocate(asset); + } + + void print_stats() { + std::cout << "Total: " << allocator_.capacity() << " bytes\n"; + std::cout << "Used: " << allocator_.used() << " bytes\n"; + std::cout << "Free: " << allocator_.available() << " bytes\n"; + std::cout << "Active allocations: " << allocator_.num_allocations() << "\n"; + } +}; +``` + +### First-Fit vs Best-Fit + +```cpp +// First-Fit: Faster allocation, may fragment more +fast_alloc::FreeListAllocator first_fit( + 1024 * 1024, + fast_alloc::FreeListStrategy::FirstFit +); + +// Best-Fit: Slower allocation, reduces fragmentation +fast_alloc::FreeListAllocator best_fit( + 1024 * 1024, + fast_alloc::FreeListStrategy::BestFit +); + +// Use First-Fit for: +// - Frequent allocations/deallocations +// - When speed is critical +// - Short-lived allocations + +// Use Best-Fit for: +// - Long-lived allocations +// - When fragmentation is a concern +// - Variable-size allocations with high reuse +``` + +## Best Practices + +### Choosing the Right Allocator + +```cpp +// Pool: Same-size objects, frequent alloc/dealloc +fast_alloc::PoolAllocator entity_pool(sizeof(Entity), 10000); + +// Stack: Temporary per-frame data, LIFO lifetime +fast_alloc::StackAllocator frame_stack(1024 * 1024); + +// Free List: Variable sizes, individual deallocation +fast_alloc::FreeListAllocator asset_allocator(100 * 1024 * 1024); +``` + +### Error Handling + +```cpp +fast_alloc::PoolAllocator pool(64, 100); + +void* ptr = pool.allocate(); +if (!ptr) { + // Pool exhausted - handle gracefully + std::cerr << "Pool allocation failed!\n"; + return; +} + +// Always check before use +use_memory(ptr); +pool.deallocate(ptr); +``` + +### Placement New Pattern + +```cpp +template +class ObjectPool { + fast_alloc::PoolAllocator pool_; + +public: + ObjectPool(size_t capacity) + : pool_(sizeof(T), capacity) {} + + template + T* create(Args&&... args) { + void* mem = pool_.allocate(); + if (!mem) return nullptr; + + // Construct object in-place + return new (mem) T(std::forward(args)...); + } + + void destroy(T* obj) { + // Explicit destructor call + obj->~T(); + pool_.deallocate(obj); + } +}; + +// Usage +ObjectPool entities(1000); +Entity* e = entities.create(x, y, z); +entities.destroy(e); +``` + +### RAII Wrapper + +```cpp +template +class ScopedAllocation { + Allocator& allocator_; + void* ptr_; + +public: + ScopedAllocation(Allocator& alloc, size_t size) + : allocator_(alloc), ptr_(allocator_.allocate(size)) {} + + ~ScopedAllocation() { + if (ptr_) { + allocator_.deallocate(ptr_); + } + } + + void* get() const { return ptr_; } + + // Prevent copy + ScopedAllocation(const ScopedAllocation&) = delete; + ScopedAllocation& operator=(const ScopedAllocation&) = delete; +}; + +// Usage +fast_alloc::PoolAllocator pool(64, 100); +{ + ScopedAllocation alloc(pool, 64); + // Use alloc.get()... +} // Automatically deallocated +``` + +## Common Patterns + +### Hybrid Approach + +```cpp +class GameEngine { + // Pool for entities (same size, frequent alloc/dealloc) + fast_alloc::PoolAllocator entity_pool_; + + // Stack for per-frame temporary data + fast_alloc::StackAllocator frame_stack_; + + // Free list for variable-size assets + fast_alloc::FreeListAllocator asset_allocator_; + +public: + GameEngine() + : entity_pool_(sizeof(Entity), 10000) + , frame_stack_(2 * 1024 * 1024) // 2MB + , asset_allocator_(100 * 1024 * 1024, // 100MB + fast_alloc::FreeListStrategy::BestFit) {} + + void update() { + // Use appropriate allocator for each task + Entity* e = create_entity(); // Pool + void* temp = frame_stack_.allocate(1024); // Stack + void* asset = load_asset(size); // Free list + + // ... game logic ... + + frame_stack_.reset(); // Clear frame data + } +}; +``` + +### Memory Budget Management + +```cpp +class MemoryManager { + fast_alloc::PoolAllocator small_pool_; + fast_alloc::PoolAllocator medium_pool_; + fast_alloc::FreeListAllocator large_allocator_; + +public: + void* allocate(size_t size) { + if (size <= 64) { + return small_pool_.allocate(); + } else if (size <= 256) { + return medium_pool_.allocate(); + } else { + return large_allocator_.allocate(size); + } + } + + void print_budget() { + size_t small_used = small_pool_.allocated() * small_pool_.block_size(); + size_t medium_used = medium_pool_.allocated() * medium_pool_.block_size(); + size_t large_used = large_allocator_.used(); + + std::cout << "Memory Budget:\n" + << " Small (<64B): " << small_used << "\n" + << " Medium (<256B): " << medium_used << "\n" + << " Large: " << large_used << "\n" + << " Total: " << (small_used + medium_used + large_used) << "\n"; + } +}; +``` + +## Performance Tips + +1. **Pre-allocate**: Size pools and stacks appropriately to avoid failures +2. **Batch operations**: Allocate/deallocate in batches when possible +3. **Reuse**: Prefer pool allocators for objects with high turnover +4. **Profile**: Measure actual performance impact in your use case +5. **Alignment**: Use appropriate alignment for SIMD and cache optimization + +## Debugging + +Enable assertions in debug builds to catch common errors: + +```cpp +// These will assert in debug builds: +pool.deallocate(invalid_ptr); // Pointer not from this pool +stack.reset(invalid_marker); // Invalid marker +allocator.allocate(0); // Zero-size allocation +``` + +Build with sanitizers to catch memory issues: + +```bash +cmake -B build -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_FLAGS="-fsanitize=address -g" +``` diff --git a/docs/architecture.md b/docs/architecture.md index 3a42156..1bdf953 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,6 +2,8 @@ This document explains the design decisions, implementation details, and trade-offs of each allocator in fast-alloc. +For practical usage examples and patterns, see [USAGE.md](USAGE.md). + ## Table of Contents - [Design Philosophy](#design-philosophy) diff --git a/src/freelist_allocator.cpp b/src/freelist_allocator.cpp index a9a9c85..b9107f8 100644 --- a/src/freelist_allocator.cpp +++ b/src/freelist_allocator.cpp @@ -104,7 +104,14 @@ namespace fast_alloc // Search for suitable block while (current_block) { - constexpr std::size_t adjustment = 0; + // Calculate adjustment needed for this block to meet alignment requirements + std::size_t adjustment = 0; + align_forward_with_header( + reinterpret_cast(current_block), + alignment, + sizeof(AllocationHeader), + adjustment + ); if (const std::size_t total_size = size + adjustment; current_block->size >= total_size) { @@ -249,13 +256,18 @@ namespace fast_alloc void FreeListAllocator::coalescence(FreeBlock* previous, FreeBlock* current) { + // Coalescence merges adjacent free blocks to reduce fragmentation. + // Free blocks are kept sorted by address to enable efficient merging. + // Merge with next block if adjacent if (current->next) { const std::size_t current_end = reinterpret_cast(current) + current->size; + // Check if current block ends exactly where next block begins if (const auto next_start = reinterpret_cast(current->next); current_end == next_start) { + // Merge: expand current block to include next block current->size += current->next->size; current->next = current->next->next; } @@ -266,8 +278,10 @@ namespace fast_alloc { const std::size_t prev_end = reinterpret_cast(previous) + previous->size; + // Check if previous block ends exactly where current block begins if (const auto current_start = reinterpret_cast(current); prev_end == current_start) { + // Merge: expand previous block to include current block previous->size += current->size; previous->next = current->next; } diff --git a/src/freelist_allocator.h b/src/freelist_allocator.h index 422d4a9..a47cded 100644 --- a/src/freelist_allocator.h +++ b/src/freelist_allocator.h @@ -5,15 +5,42 @@ namespace fast_alloc { + /** + * @brief Allocation strategy for free list allocator. + */ enum class FreeListStrategy { - FirstFit, // Find first block that fits - BestFit // Find the smallest block that fits + FirstFit, ///< Find first block that fits (faster, may fragment more) + BestFit ///< Find smallest block that fits (slower, reduces fragmentation) }; + /** + * @brief General-purpose allocator supporting variable-sized allocations. + * + * Maintains a linked list of free memory blocks and supports individual + * deallocation. Automatically coalesces adjacent free blocks to reduce + * fragmentation. + * + * Ideal for: game assets, dynamic strings, script objects, UI elements, + * any scenario requiring variable-sized allocations with individual frees. + * + * @note Thread-safety: Not thread-safe. + * @note Memory overhead: 16 bytes per allocation (AllocationHeader). + * @note Fragmentation: Mitigated by automatic coalescence. + * @note Performance: O(n) allocation/deallocation (searches free list). + * + * @warning Not suitable for real-time systems requiring deterministic timing. + */ class FreeListAllocator { public: + /** + * @brief Construct a free list allocator. + * + * @param size Total size in bytes of memory to manage + * @param strategy Allocation strategy (FirstFit or BestFit) + * @throws assert if size <= sizeof(FreeBlock) + */ explicit FreeListAllocator(std::size_t size, FreeListStrategy strategy = FreeListStrategy::FirstFit); ~FreeListAllocator(); @@ -25,25 +52,61 @@ namespace fast_alloc FreeListAllocator(FreeListAllocator&& other) noexcept; FreeListAllocator& operator=(FreeListAllocator&& other) noexcept; + /** + * @brief Allocate memory block. + * + * @param size Number of bytes to allocate (must be > 0) + * @param alignment Memory alignment requirement (default: alignof(std::max_align_t)) + * @return Pointer to allocated memory, or nullptr if no suitable block found + * @note Complexity: O(n) where n is number of free blocks + * + * The allocator will search the free list using the configured strategy: + * - FirstFit: Returns first block large enough (faster) + * - BestFit: Returns smallest block large enough (less fragmentation) + */ void* allocate(std::size_t size, std::size_t alignment = alignof(std::max_align_t)); + + /** + * @brief Deallocate memory block. + * + * Automatically coalesces with adjacent free blocks to reduce fragmentation. + * + * @param ptr Pointer to memory (must be from this allocator). nullptr is safely ignored. + * @note Complexity: O(n) where n is number of free blocks + */ void deallocate(void* ptr); + /** @brief Get total capacity in bytes. */ [[nodiscard]] std::size_t capacity() const noexcept { return size_; } + + /** @brief Get currently used bytes (including headers). */ [[nodiscard]] std::size_t used() const noexcept { return used_memory_; } + + /** @brief Get available bytes remaining. */ [[nodiscard]] std::size_t available() const noexcept { return size_ - used_memory_; } + + /** @brief Get number of active allocations. */ [[nodiscard]] std::size_t num_allocations() const noexcept { return num_allocations_; } private: + /** + * @brief Header stored before each allocation. + * Contains size and alignment adjustment information. + */ struct AllocationHeader { - std::size_t size; - std::size_t adjustment; + std::size_t size; ///< Total size including header and adjustment + std::size_t adjustment; ///< Bytes added for alignment }; + /** + * @brief Free block node in the intrusive free list. + * Stored in the free memory itself. + */ struct FreeBlock { - std::size_t size; - FreeBlock* next; + std::size_t size; ///< Size of this free block + FreeBlock* next; ///< Next free block (sorted by address) }; std::size_t size_; @@ -51,9 +114,25 @@ namespace fast_alloc std::size_t num_allocations_; FreeListStrategy strategy_; void* memory_; - FreeBlock* free_blocks_; + FreeBlock* free_blocks_; ///< Head of free list (sorted by address for coalescence) + /** + * @brief Merge adjacent free blocks. + * + * @param previous Previous free block in list + * @param current Current free block to potentially merge + */ static void coalescence(FreeBlock* previous, FreeBlock* current); + + /** + * @brief Calculate aligned address accounting for header. + * + * @param address Starting address + * @param alignment Desired alignment + * @param header_size Size of allocation header + * @param[out] adjustment Total adjustment needed (header + padding) + * @return Aligned address after header + */ static std::size_t align_forward_with_header( std::size_t address, std::size_t alignment, diff --git a/src/pool_allocator.cpp b/src/pool_allocator.cpp index e94b0b4..17c9104 100644 --- a/src/pool_allocator.cpp +++ b/src/pool_allocator.cpp @@ -118,6 +118,22 @@ namespace fast_alloc assert(allocated_count_ > 0 && "Deallocating from empty pool"); + // Validate pointer is within our memory range + const auto ptr_address = reinterpret_cast(ptr); + const auto memory_start = reinterpret_cast(memory_); + const auto memory_end = memory_start + (block_size_ * block_count_); + + assert(ptr_address >= memory_start && ptr_address < memory_end + && "Pointer outside pool memory range"); + + // Validate pointer is properly aligned to a block boundary + assert((ptr_address - memory_start) % block_size_ == 0 + && "Pointer not aligned to block boundary"); + + // Suppress unused variable warnings in release builds + (void)ptr_address; + (void)memory_end; + // Push back to free list const auto block = static_cast(ptr); *block = free_list_; diff --git a/src/pool_allocator.h b/src/pool_allocator.h index febc92e..5f64dc6 100644 --- a/src/pool_allocator.h +++ b/src/pool_allocator.h @@ -5,9 +5,28 @@ namespace fast_alloc { + /** + * @brief Fixed-size block memory pool allocator. + * + * Extremely fast O(1) allocation/deallocation for objects of uniform size. + * Ideal for particle systems, game entities, audio voices, and network packets. + * + * @note Thread-safety: Not thread-safe. Use ThreadSafePoolAllocator for concurrent access. + * @note Memory overhead: 0 bytes per allocation (uses free space for intrusive list). + * @note Fragmentation: None (all blocks same size). + * + * @warning Block size must be at least sizeof(void*) to store free list pointers. + */ class PoolAllocator { public: + /** + * @brief Construct a pool allocator. + * + * @param block_size Size in bytes of each block (must be >= sizeof(void*)) + * @param block_count Number of blocks to allocate + * @throws assert if block_size < sizeof(void*) or block_count == 0 + */ PoolAllocator(std::size_t block_size, std::size_t block_count); ~PoolAllocator(); @@ -19,12 +38,33 @@ namespace fast_alloc PoolAllocator(PoolAllocator&& other) noexcept; PoolAllocator& operator=(PoolAllocator&& other) noexcept; + /** + * @brief Allocate a single block from the pool. + * + * @return Pointer to allocated block, or nullptr if pool is exhausted. + * @note Complexity: O(1) - single pointer dereference + */ void* allocate(); + + /** + * @brief Return a block to the pool. + * + * @param ptr Pointer to block (must be from this allocator). nullptr is safely ignored. + * @note Complexity: O(1) - two pointer assignments + * @warning Passing invalid pointers will trigger assertions in debug builds. + */ void deallocate(void* ptr); + /** @brief Get the size of each block in bytes. */ [[nodiscard]] std::size_t block_size() const noexcept { return block_size_; } + + /** @brief Get the total capacity (number of blocks). */ [[nodiscard]] std::size_t capacity() const noexcept { return block_count_; } + + /** @brief Get the number of currently allocated blocks. */ [[nodiscard]] std::size_t allocated() const noexcept { return allocated_count_; } + + /** @brief Check if the pool is full (no blocks available). */ [[nodiscard]] bool is_full() const noexcept { return allocated_count_ >= block_count_; } private: @@ -32,6 +72,6 @@ namespace fast_alloc std::size_t block_count_; std::size_t allocated_count_; void* memory_; - void* free_list_; + void* free_list_; // Intrusive linked list of free blocks }; } // namespace fast_alloc diff --git a/src/stack_allocator.h b/src/stack_allocator.h index 3cf172b..76521ee 100644 --- a/src/stack_allocator.h +++ b/src/stack_allocator.h @@ -5,9 +5,30 @@ namespace fast_alloc { + /** + * @brief Linear (stack-based) allocator with frame reset capability. + * + * Extremely fast O(1) allocation using pointer bumping. Perfect for temporary + * per-frame allocations with LIFO (last-in-first-out) lifetime. + * + * Ideal for: rendering command lists, string formatting, scratch buffers, + * temporary calculations within a frame or function scope. + * + * @note Thread-safety: Not thread-safe. + * @note Memory overhead: 0 bytes per allocation. + * @note Fragmentation: None. + * + * @warning Cannot deallocate individual allocations - only reset to marker or beginning. + */ class StackAllocator { public: + /** + * @brief Construct a stack allocator. + * + * @param size Total size in bytes of the stack memory + * @throws assert if size == 0 + */ explicit StackAllocator(std::size_t size); ~StackAllocator(); @@ -19,23 +40,65 @@ namespace fast_alloc StackAllocator(StackAllocator&& other) noexcept; StackAllocator& operator=(StackAllocator&& other) noexcept; + /** + * @brief Allocate memory from the stack. + * + * @param size Number of bytes to allocate + * @param alignment Memory alignment requirement (default: alignof(std::max_align_t)) + * @return Pointer to allocated memory, or nullptr if insufficient space + * @note Complexity: O(1) - pointer arithmetic only + * + * Example: + * @code + * void* ptr = stack.allocate(256, 16); // 256 bytes, 16-byte aligned + * @endcode + */ void* allocate(std::size_t size, std::size_t alignment = alignof(std::max_align_t)); - // Reset to a previous marker or to the beginning + /** + * @brief Reset allocator to a previous state or to the beginning. + * + * @param marker Position to reset to (from get_marker()), or nullptr to reset to start + * @note Complexity: O(1) - single pointer assignment + * + * Example: + * @code + * void* marker = stack.get_marker(); + * // ... allocations ... + * stack.reset(marker); // Rewind to marker + * stack.reset(); // Reset to beginning + * @endcode + */ void reset(void* marker = nullptr); - // Get current position for later reset + /** + * @brief Get current position marker for later reset. + * + * @return Opaque marker representing current stack position + * @note Use with reset() to implement scoped memory allocation + */ [[nodiscard]] void* get_marker() const noexcept { return current_; } + /** @brief Get total capacity in bytes. */ [[nodiscard]] std::size_t capacity() const noexcept { return size_; } + + /** @brief Get currently used bytes. */ [[nodiscard]] std::size_t used() const noexcept; + + /** @brief Get available bytes remaining. */ [[nodiscard]] std::size_t available() const noexcept; private: std::size_t size_; void* memory_; - void* current_; + void* current_; // Current top of stack + /** + * @brief Align address forward to meet alignment requirement. + * @param address Address to align + * @param alignment Alignment requirement (must be power of 2) + * @return Aligned address + */ [[nodiscard]] static std::size_t align_forward(std::size_t address, std::size_t alignment) noexcept; }; } // namespace fast_alloc diff --git a/src/threadsafe_pool_allocator.cpp b/src/threadsafe_pool_allocator.cpp index fcd4c47..7c406f7 100644 --- a/src/threadsafe_pool_allocator.cpp +++ b/src/threadsafe_pool_allocator.cpp @@ -78,6 +78,22 @@ namespace fast_alloc std::lock_guard lock(mutex_); + // Validate pointer is within our memory range + const auto ptr_address = reinterpret_cast(ptr); + const auto memory_start = reinterpret_cast(memory_); + const auto memory_end = memory_start + (block_size_ * block_count_); + + assert(ptr_address >= memory_start && ptr_address < memory_end + && "Pointer outside pool memory range"); + + // Validate pointer is properly aligned to a block boundary + assert((ptr_address - memory_start) % block_size_ == 0 + && "Pointer not aligned to block boundary"); + + // Suppress unused variable warnings in release builds + (void)ptr_address; + (void)memory_end; + *static_cast(ptr) = free_list_.load(std::memory_order_relaxed); free_list_.store(ptr, std::memory_order_relaxed); allocated_count_.fetch_sub(1, std::memory_order_relaxed); diff --git a/src/threadsafe_pool_allocator.h b/src/threadsafe_pool_allocator.h index fcc61d9..8b66966 100644 --- a/src/threadsafe_pool_allocator.h +++ b/src/threadsafe_pool_allocator.h @@ -7,9 +7,33 @@ namespace fast_alloc { + /** + * @brief Thread-safe fixed-size block memory pool allocator. + * + * Thread-safe variant of PoolAllocator using mutex protection. + * Provides safe concurrent access at the cost of additional synchronization overhead. + * + * Ideal for: multithreaded particle systems, concurrent audio processing, + * network packet pools accessed by multiple threads. + * + * @note Thread-safety: Fully thread-safe using std::mutex. + * @note Memory overhead: 0 bytes per allocation (uses free space for intrusive list). + * @note Fragmentation: None (all blocks same size). + * @note Performance: Slightly slower than PoolAllocator due to mutex overhead. + * + * @warning Move operations are disabled to prevent unsafe concurrent access. + * @warning Block size must be at least sizeof(void*) to store free list pointers. + */ class ThreadSafePoolAllocator { public: + /** + * @brief Construct a thread-safe pool allocator. + * + * @param block_size Size in bytes of each block (must be >= sizeof(void*)) + * @param block_count Number of blocks to allocate + * @throws assert if block_size < sizeof(void*) or block_count == 0 + */ ThreadSafePoolAllocator(std::size_t block_size, std::size_t block_count); ~ThreadSafePoolAllocator(); @@ -17,32 +41,56 @@ namespace fast_alloc ThreadSafePoolAllocator(const ThreadSafePoolAllocator&) = delete; ThreadSafePoolAllocator& operator=(const ThreadSafePoolAllocator&) = delete; - // Disable move + // Disable move (unsafe with mutex) ThreadSafePoolAllocator(ThreadSafePoolAllocator&&) = delete; ThreadSafePoolAllocator& operator=(ThreadSafePoolAllocator&&) = delete; + /** + * @brief Allocate a single block from the pool (thread-safe). + * + * @return Pointer to allocated block, or nullptr if pool is exhausted. + * @note Complexity: O(1) + mutex lock overhead + * @note Thread-safe: Yes + */ void* allocate(); + + /** + * @brief Return a block to the pool (thread-safe). + * + * @param ptr Pointer to block (must be from this allocator). nullptr is safely ignored. + * @note Complexity: O(1) + mutex lock overhead + * @note Thread-safe: Yes + * @warning Passing invalid pointers will trigger assertions in debug builds. + */ void deallocate(void* ptr); + /** @brief Get the size of each block in bytes (thread-safe). */ [[nodiscard]] std::size_t block_size() const noexcept { return block_size_; } + + /** @brief Get the total capacity (number of blocks) (thread-safe). */ [[nodiscard]] std::size_t capacity() const noexcept { return block_count_; } + /** + * @brief Get the number of currently allocated blocks (thread-safe). + * @note Uses relaxed memory ordering for performance. + */ [[nodiscard]] std::size_t allocated() const noexcept { return allocated_count_.load(std::memory_order_relaxed); } + /** @brief Check if the pool is full (thread-safe). */ [[nodiscard]] bool is_full() const noexcept { return allocated() >= block_count_; } private: - mutable std::mutex mutex_; - std::size_t block_size_; - std::size_t block_count_; - std::atomic allocated_count_; - void* memory_; - std::atomic free_list_; + mutable std::mutex mutex_; ///< Mutex protecting allocate/deallocate operations + std::size_t block_size_; ///< Size of each block + std::size_t block_count_; ///< Total number of blocks + std::atomic allocated_count_; ///< Current allocation count + void* memory_; ///< Base memory pointer + std::atomic free_list_; ///< Head of intrusive free list }; } // namespace fast_alloc