Created
May 22, 2026 19:46
-
-
Save unrays/7ed8272cbc57faec5aa482a400a0ffa5 to your computer and use it in GitHub Desktop.
Unsynchronized chunk allocator built on a custom memory_resource, featuring monotonic chunk allocation, alignment handling, and basic object construction utilities.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright (c) May 2026 Félix-Olivier Dumas. All rights reserved. | |
| // Licensed under the terms described in the LICENSE file | |
| #pragma once | |
| // This is my own custom memory_resource, but you can either use your own | |
| // or the one from the standard library (std::pmr). | |
| #include "memory_resource.hpp" | |
| #include <cstddef> | |
| #include <iostream> | |
| #include <new> | |
| #include <stdexcept> | |
| #include <utility> | |
| #include <vector> | |
| namespace exotic::memory { | |
| template<typename Tp> | |
| struct unsynchronized_chunk_allocator { | |
| private: | |
| static constexpr std::size_t default_chunk_capacity = 1024; | |
| static constexpr std::size_t default_initial_reserve = 10; | |
| static constexpr std::size_t default_chunk_refill = 5; | |
| static constexpr std::size_t default_chunk_refill_treshold = 1; | |
| public: | |
| struct Chunk { | |
| std::byte* buffer_ = nullptr; | |
| std::size_t capacity_ = 0; | |
| std::size_t offset_ = 0; | |
| }; | |
| public: | |
| explicit unsynchronized_chunk_allocator(memory_resource* upstream, std::size_t reserve = default_initial_reserve) | |
| : ressource_{ upstream } | |
| { | |
| if (upstream == nullptr) [[unlikely]] | |
| throw std::invalid_argument("Upstream memory resource cannot be null"); | |
| if (reserve == 0) [[unlikely]] | |
| throw std::invalid_argument("reserve must be >= 1"); | |
| free_chunks_.reserve(std::size_t{ default_initial_reserve }); | |
| for (std::size_t i = 0; i < reserve - 1; ++i) { | |
| void* raw = ressource_->allocate(sizeof(Tp) * default_chunk_capacity, alignof(Tp)); | |
| free_chunks_.emplace_back(Chunk{ static_cast<std::byte*>(raw), default_chunk_capacity, std::size_t{0} }); | |
| } | |
| void* raw = ressource_->allocate(sizeof(Tp) * default_chunk_capacity, alignof(Tp)); | |
| active_chunk_ = Chunk{ static_cast<std::byte*>(raw), default_chunk_capacity, std::size_t{0} }; | |
| } | |
| unsynchronized_chunk_allocator(const unsynchronized_chunk_allocator&) = delete; | |
| unsynchronized_chunk_allocator& operator=(const unsynchronized_chunk_allocator&) = delete; | |
| unsynchronized_chunk_allocator(unsynchronized_chunk_allocator&&) noexcept = default; | |
| unsynchronized_chunk_allocator& operator=(unsynchronized_chunk_allocator&&) noexcept = default; | |
| ~unsynchronized_chunk_allocator() noexcept { | |
| const std::size_t chunk_bytes = sizeof(Tp) * default_chunk_capacity; | |
| const std::size_t chunk_align = alignof(Tp); | |
| for (auto& chunk : free_chunks_) { | |
| if (chunk.buffer_ != nullptr) { | |
| ressource_->deallocate(static_cast<void*>(chunk.buffer_), chunk_bytes, chunk_align); | |
| } | |
| } | |
| if (active_chunk_.buffer_ != nullptr) [[likely]] { | |
| ressource_->deallocate(static_cast<void*>(active_chunk_.buffer_), chunk_bytes, chunk_align); | |
| } | |
| } | |
| public: | |
| void refill(std::size_t count = default_chunk_refill) { | |
| for (std::size_t i = 0; i < count; ++i) { | |
| void* raw = ressource_->allocate(sizeof(Tp) * default_chunk_capacity, alignof(Tp)); | |
| free_chunks_.emplace_back(Chunk{ static_cast<std::byte*>(raw), default_chunk_capacity, std::size_t{0} }); | |
| } | |
| } | |
| public: | |
| [[nodiscard]] Tp* allocate(std::size_t count) { | |
| if (free_chunks_.size() <= default_chunk_refill_treshold) [[unlikely]] { | |
| refill(); | |
| } | |
| if (active_chunk_.buffer_ == nullptr) [[unlikely]] { | |
| if (free_chunks_.empty()) [[unlikely]] throw std::bad_alloc(); | |
| active_chunk_ = std::move(free_chunks_.back()); | |
| free_chunks_.pop_back(); | |
| } | |
| const std::size_t requested = (sizeof(Tp) * count); | |
| std::size_t aligned = (active_chunk_.offset_ + (alignof(Tp) - 1)) & ~(alignof(Tp) - 1); | |
| std::size_t next_offset = aligned + requested; | |
| if (next_offset >= active_chunk_.capacity_) { | |
| if (free_chunks_.empty()) [[unlikely]] throw std::bad_alloc(); | |
| active_chunk_ = std::move(free_chunks_.back()); | |
| free_chunks_.pop_back(); | |
| aligned = (active_chunk_.offset_ + (alignof(Tp) - 1)) & ~(alignof(Tp) - 1); | |
| next_offset = aligned + requested; | |
| if (next_offset > active_chunk_.capacity_) [[unlikely]] throw std::bad_alloc(); | |
| } | |
| active_chunk_.offset_ = next_offset; | |
| return reinterpret_cast<Tp*>(active_chunk_.buffer_ + aligned); | |
| } | |
| [[nodiscard]] void* allocate_bytes(std::size_t bytes, std::size_t alignment) { | |
| if (free_chunks_.size() <= default_chunk_refill_treshold) [[unlikely]] { | |
| refill(); | |
| } | |
| if (active_chunk_.buffer_ == nullptr) [[unlikely]] { | |
| if (free_chunks_.empty()) [[unlikely]] throw std::bad_alloc(); | |
| active_chunk_ = std::move(free_chunks_.back()); | |
| free_chunks_.pop_back(); | |
| } | |
| std::size_t aligned = (active_chunk_.offset_ + (alignment - 1)) & ~(alignment - 1); | |
| std::size_t next_offset = aligned + bytes; | |
| if (next_offset >= active_chunk_.capacity_) { | |
| if (free_chunks_.empty()) [[unlikely]] throw std::bad_alloc(); | |
| active_chunk_ = std::move(free_chunks_.back()); | |
| free_chunks_.pop_back(); | |
| aligned = (active_chunk_.offset_ + (alignment - 1)) & ~(alignment - 1); | |
| next_offset = aligned + bytes; | |
| if (next_offset > active_chunk_.capacity_) [[unlikely]] throw std::bad_alloc(); | |
| } | |
| active_chunk_.offset_ = next_offset; | |
| return reinterpret_cast<void*>(active_chunk_.buffer_ + aligned); | |
| } | |
| template<typename Up> | |
| [[nodiscard]] Up* allocate_object(std::size_t count = 1) { | |
| void* const raw = allocate_bytes(sizeof(Up) * count, alignof(Up)); | |
| return static_cast<Up*>(raw); | |
| } | |
| public: | |
| void deallocate( | |
| [[maybe_unused]] Tp* const ptr, | |
| [[maybe_unused]] std::size_t count | |
| ) noexcept { | |
| // NO-OP: This chunk allocator does not support individual deallocations. | |
| // All memory is reclaimed at once via the global reset() method. | |
| } | |
| void deallocate_bytes( | |
| [[maybe_unused]] void* const ptr, | |
| [[maybe_unused]] std::size_t bytes, | |
| [[maybe_unused]] std::size_t alignment | |
| ) noexcept { | |
| // NO-OP: This chunk allocator does not support byte deallocations. | |
| // All memory is reclaimed at once via the global reset() method. | |
| } | |
| template<typename Up> | |
| void deallocate_object( | |
| [[maybe_unused]] Up* const ptr, | |
| [[maybe_unused]] std::size_t count = 1 | |
| ) noexcept { | |
| // NO-OP: This chunk allocator does not support object deallocations. | |
| // All memory is reclaimed at once via the global reset() method. | |
| } | |
| public: | |
| template<typename Up, typename... Types> | |
| void construct(Up* const ptr, Types&&... args) { | |
| if (!ptr) [[unlikely]] throw std::bad_alloc(); | |
| ::new (const_cast<void*>(static_cast<const volatile void*>(ptr))) | |
| Up(std::forward<Types>(args)...); | |
| } | |
| template<typename Up> | |
| void destroy(Up* const ptr) noexcept { | |
| if (!ptr) [[unlikely]] throw std::bad_alloc(); | |
| ptr->~Up(); | |
| } | |
| public: | |
| template<typename Up, typename... Types> | |
| [[nodiscard]] Up* new_object(Types&&... args) { | |
| Up* const ptr = allocate_object<Up>(); | |
| construct(ptr, std::forward<Types>(args)...); | |
| return ptr; | |
| } | |
| template<typename Up> | |
| void delete_object(Up* const ptr) { | |
| destroy(ptr); | |
| deallocate_object<Up>(); | |
| } | |
| private: | |
| std::vector<Chunk> free_chunks_; | |
| Chunk active_chunk_; | |
| memory_resource* ressource_; | |
| }; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment