From 3742f7e1a0d5b4954d067c67856fe3632024e36f Mon Sep 17 00:00:00 2001 From: B33Boy Date: Mon, 27 Apr 2026 11:34:11 -0400 Subject: [PATCH 1/7] feat: linear allocator with tests --- memory/linear_allocator.cpp | 127 ++++++++++++++++++++++++++++++++ memory/linear_allocator.hpp | 142 ++++++++++++++++++++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 memory/linear_allocator.cpp create mode 100644 memory/linear_allocator.hpp diff --git a/memory/linear_allocator.cpp b/memory/linear_allocator.cpp new file mode 100644 index 0000000000..3559520168 --- /dev/null +++ b/memory/linear_allocator.cpp @@ -0,0 +1,127 @@ +/** + * @file + * @brief Linear Allocator + * @details Implementation of a linear allocator that pre-allocates memory + * for high speed access. See [Region-based memory + * management](https://en.wikipedia.org/wiki/Region-based_memory_management). + * + * A linear (also known a bump) allocator works by allocating a buffer of + * memory upon initialization, and then handing it out per allocation call. An + * index is used to indicate from where to hand out memory, after doing so it + * will be "bumped up" to the next spot. For this implementation, the next spot + * is ensured to be aligned to the largest type on this plaform. + * + * Note: CPUs access data most efficiently when it is aligned (i.e. data of + * size N is located at a memory address that is a multiple of N). Alignment of + * data matters because depending on the platform, when accessing un-aligned + * data a CPU will have reduced performance or straight up crash. See + * https://en.wikipedia.org/wiki/Data_structure_alignment for more info. + * + * One of the biggest reasons for using a compile-time linear allocator is the + * fact that lifetime of data is limited to the scope of use. We don't have to + * worry about freeing up memory as the stack will take care of that for us, and + * for that reason there is no need for a de-allocate function. + * + * + * @details + * Key constraints and limitations of this example: + * - As mentioned above, this implementation has the buffer aligned to the + * std::max_align_t which ensures that fundamental types are aligned to the + * largest type on the current platform. However, when trying to allocate for + * complex types that are larger, this isn't enough. One solution would be + * to take a note from + * [std::pmr::memory_resource::allocate](https://en.cppreference.com/cpp/memory/memory_resource/allocate) + * and have the alignment as a default parameter. + * + * - It is up to the user to ensure that they do not write past the returned + * bytes. This is the same behaviour as "malloc" or "new". + * + * + * @author [Abhi Patel](https://github.com/B33Boy/) + */ + +#include "linear_allocator.hpp" + +#include /// for std::assert +#include /// for IO operations + +/** + * @brief Memory algorithms + * @namespace memory + */ +namespace memory { + +/** + * @brief allocator algorithms + * @namespace allocator + */ +namespace allocator { + +/** + * @brief Self-test implementations + * @returns void + */ + +static void test_allocator_allocates_data() { + static const size_t CAPACITY = 16; + linear_allocator la{}; + + auto* mem = la.allocate(sizeof(int)); + int* num_ptr = new (mem) int{69}; ///< use placement new to initialize an + ///< int at the memory we allocated + + assert(69 == *num_ptr); + assert(4 == la.size()); + assert(16 == la.cap()); +} + +static void test_allocator_resets() { + static const size_t CAPACITY = 8; + linear_allocator la{}; + + auto* mem = la.allocate(sizeof(int)); + int* num_ptr = new (mem) int{69}; + + la.reset(); + assert(0 == la.size()); + + // Allocate memory at the same location as the first, should hand out the + // same bytes + auto* new_mem = la.allocate(sizeof(int)); + int* new_num_ptr = new (new_mem) int{10}; + + assert(10 == *new_num_ptr); + assert(*new_num_ptr == *num_ptr); +} + +static void test_allocate_when_full_returns_nullptr() { + static const size_t CAPACITY = sizeof(double); + linear_allocator la{}; + + auto* mem = la.allocate(sizeof(double)); + double* _ = new (mem) double{69.0}; + + auto* some_char = la.allocate(sizeof(char)); + assert(nullptr == some_char); +} + +static void tests() { + test_allocator_allocates_data(); + test_allocator_resets(); + test_allocate_when_full_returns_nullptr(); + + std::cout << "All tests have successfully passed!\n"; +} + +/** + * @brief Main function + * @returns 0 on exit + */ +int main() { + tests(); + + return 0; +} + +} // namespace allocator +} // namespace memory \ No newline at end of file diff --git a/memory/linear_allocator.hpp b/memory/linear_allocator.hpp new file mode 100644 index 0000000000..654251bab1 --- /dev/null +++ b/memory/linear_allocator.hpp @@ -0,0 +1,142 @@ +/** + * @file + * @brief Linear Allocator + * @details Implementation of a linear allocator that pre-allocates memory + * for high speed access. See [Region-based memory + * management](https://en.wikipedia.org/wiki/Region-based_memory_management). + * + * A linear (also known a bump) allocator works by allocating a buffer of + * memory upon initialization, and then handing it out per allocation call. An + * index is used to indicate from where to hand out memory, after doing so it + * will be "bumped up" to the next spot. For this implementation, the next spot + * is ensured to be aligned to the largest type on this plaform. + * + * Note: CPUs access data most efficiently when it is aligned (i.e. data of + * size N is located at a memory address that is a multiple of N). Alignment of + * data matters because depending on the platform, when accessing un-aligned + * data a CPU will have reduced performance or straight up crash. See + * https://en.wikipedia.org/wiki/Data_structure_alignment for more info. + * + * One of the biggest reasons for using a compile-time linear allocator is the + * fact that lifetime of data is limited to the scope of use. We don't have to + * worry about freeing up memory as the stack will take care of that for us, and + * for that reason there is no need for a de-allocate function. + * + * + * @details + * Key constraints and limitations of this example: + * - As mentioned above, this implementation has the buffer aligned to the + * std::max_align_t which ensures that fundamental types are aligned to the + * largest type on the current platform. However, when trying to allocate for + * complex types which may be larger, this doesn't work. One solution would be + * to take a note from * + * [std::pmr::memory_resource::allocate](https://en.cppreference.com/cpp/memory/memory_resource/allocate) + * and have the alignment as a default parameter. + * + * - It is up to the user to ensure that they do not write past the returned + * bytes. This is the same behaviour as "malloc" or "new". + * + * + * @author [Abhi Patel](https://github.com/B33Boy/) + */ + +#pragma once + +#include /// for std::byte + +/** + * @brief Memory algorithms + * @namespace memory + */ +namespace memory { +/** + * @brief allocator algorithm + * @namespace allocator + */ +namespace allocator { + +/** + * @class linear_allocator + * @brief Stack based allocator that is used to hand out bytes of memory + * + * @tparam N + */ +template +class linear_allocator { + public: + constexpr linear_allocator() = default; + + // ================= Special Member Functions ================= + /** + * For simplicity, we will disallow copying or moving the allocator. + */ + + ~linear_allocator() = default; + + linear_allocator(linear_allocator const&) = + delete; ///< disable copy constructor + + linear_allocator(linear_allocator&&) noexcept = + delete; ///< disable move constructor + + linear_allocator& operator=(linear_allocator const&) = + delete; ///< disable copy assignment operator + + linear_allocator& operator=(linear_allocator&&) noexcept = + delete; ///< disable move assignment operator + + // ================= Allocataor API ================= + /** + * @brief Allocates num_bytes from the internal buffer + * @details Returns a pointer to the next aligned position in the + * buffer. + * + * @param num_bytes number of bytes to allocate + * @return std::byte* to allocate memory, or nullptr upon failure + */ + [[nodiscard]] auto constexpr allocate(size_t num_bytes) noexcept + -> std::byte* { + size_t aligned_next = linear_allocator::align(next_ + num_bytes); + + if (aligned_next > N) + return nullptr; + + auto* mem = &buffer_[next_]; + next_ = aligned_next; + + return mem; + } + + /** + * @brief resets allocator to the start + * + */ + void reset() noexcept { next_ = 0; } + + /** + * @brief returns size of data allocated + * + * @return size_t next position + */ + auto size() const noexcept -> size_t { return next_; } + + /** + * @brief return capacity of the allocator + * + * @return size_t capacity + */ + auto cap() const noexcept -> size_t { return N; } + + private: + static constexpr size_t alignment = alignof(std::max_align_t); + + alignas(alignment) std::byte buffer_[N]; + size_t next_{0}; + + static constexpr auto align(size_t offset) noexcept -> size_t { + return (offset + (alignment - 1)) & ~(alignment - 1); + } +}; + +} // namespace allocator +} // namespace memory From 637fbada0913b8bdd35e8b0f7f0eb81dc6821ca4 Mon Sep 17 00:00:00 2001 From: B33Boy Date: Mon, 27 Apr 2026 11:34:43 -0400 Subject: [PATCH 2/7] chore: configure CMake --- CMakeLists.txt | 1 + memory/CMakeLists.txt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 memory/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fb339e475..0c7f57fd26 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,7 @@ add_subdirectory(range_queries) add_subdirectory(search) add_subdirectory(sorting) add_subdirectory(strings) +add_subdirectory(memory) cmake_policy(SET CMP0054 NEW) cmake_policy(SET CMP0057 NEW) diff --git a/memory/CMakeLists.txt b/memory/CMakeLists.txt new file mode 100644 index 0000000000..17ae8f16bf --- /dev/null +++ b/memory/CMakeLists.txt @@ -0,0 +1,18 @@ +# If necessary, use the RELATIVE flag, otherwise each source file may be listed +# with full pathname. RELATIVE may makes it easier to extract an executable name +# automatically. +file( GLOB APP_SOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.cpp ) +# file( GLOB APP_SOURCES ${CMAKE_SOURCE_DIR}/*.c ) +# AUX_SOURCE_DIRECTORY(${CMAKE_CURRENT_SOURCE_DIR} APP_SOURCES) +foreach( testsourcefile ${APP_SOURCES} ) + # I used a simple string replace, to cut off .cpp. + string( REPLACE ".cpp" "" testname ${testsourcefile} ) + add_executable( ${testname} ${testsourcefile} ) + + set_target_properties(${testname} PROPERTIES LINKER_LANGUAGE CXX) + if(OpenMP_CXX_FOUND) + target_link_libraries(${testname} OpenMP::OpenMP_CXX) + endif() + install(TARGETS ${testname} DESTINATION "bin/memory") + +endforeach( testsourcefile ${APP_SOURCES} ) From 56231a2d7ab5d6f75d4c3578f35f908bc1c94fd8 Mon Sep 17 00:00:00 2001 From: B33Boy Date: Mon, 27 Apr 2026 11:47:22 -0400 Subject: [PATCH 3/7] docs: update explanation --- memory/linear_allocator.cpp | 5 ++--- memory/linear_allocator.hpp | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/memory/linear_allocator.cpp b/memory/linear_allocator.cpp index 3559520168..d55d2ca057 100644 --- a/memory/linear_allocator.cpp +++ b/memory/linear_allocator.cpp @@ -23,13 +23,12 @@ * for that reason there is no need for a de-allocate function. * * - * @details * Key constraints and limitations of this example: * - As mentioned above, this implementation has the buffer aligned to the * std::max_align_t which ensures that fundamental types are aligned to the * largest type on the current platform. However, when trying to allocate for - * complex types that are larger, this isn't enough. One solution would be - * to take a note from + * complex types which may be larger, this doesn't work. One solution would be + * to take a note from * * [std::pmr::memory_resource::allocate](https://en.cppreference.com/cpp/memory/memory_resource/allocate) * and have the alignment as a default parameter. * diff --git a/memory/linear_allocator.hpp b/memory/linear_allocator.hpp index 654251bab1..d30bb8df80 100644 --- a/memory/linear_allocator.hpp +++ b/memory/linear_allocator.hpp @@ -23,7 +23,6 @@ * for that reason there is no need for a de-allocate function. * * - * @details * Key constraints and limitations of this example: * - As mentioned above, this implementation has the buffer aligned to the * std::max_align_t which ensures that fundamental types are aligned to the From 75e0634fcca40327d3793d65bdb918ddeaa0b609 Mon Sep 17 00:00:00 2001 From: B33Boy Date: Mon, 27 Apr 2026 11:57:48 -0400 Subject: [PATCH 4/7] fix: avoid cmake warning --- memory/linear_allocator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/memory/linear_allocator.cpp b/memory/linear_allocator.cpp index d55d2ca057..42fa94dc2b 100644 --- a/memory/linear_allocator.cpp +++ b/memory/linear_allocator.cpp @@ -98,7 +98,7 @@ static void test_allocate_when_full_returns_nullptr() { linear_allocator la{}; auto* mem = la.allocate(sizeof(double)); - double* _ = new (mem) double{69.0}; + [[maybe_unused]] double* _ = new (mem) double{69.0}; auto* some_char = la.allocate(sizeof(char)); assert(nullptr == some_char); From 07c8e5b17b1164dbf2876c25448448bfbcb7d60e Mon Sep 17 00:00:00 2001 From: B33Boy Date: Mon, 27 Apr 2026 17:54:35 -0400 Subject: [PATCH 5/7] fix: allocator tests --- memory/linear_allocator.cpp | 56 ++++++++++++------------------------- memory/linear_allocator.hpp | 2 +- 2 files changed, 19 insertions(+), 39 deletions(-) diff --git a/memory/linear_allocator.cpp b/memory/linear_allocator.cpp index 42fa94dc2b..e28fe008eb 100644 --- a/memory/linear_allocator.cpp +++ b/memory/linear_allocator.cpp @@ -28,7 +28,7 @@ * std::max_align_t which ensures that fundamental types are aligned to the * largest type on the current platform. However, when trying to allocate for * complex types which may be larger, this doesn't work. One solution would be - * to take a note from * + * to take a note from * [std::pmr::memory_resource::allocate](https://en.cppreference.com/cpp/memory/memory_resource/allocate) * and have the alignment as a default parameter. * @@ -44,17 +44,7 @@ #include /// for std::assert #include /// for IO operations -/** - * @brief Memory algorithms - * @namespace memory - */ -namespace memory { - -/** - * @brief allocator algorithms - * @namespace allocator - */ -namespace allocator { +using namespace memory::allocator; /** * @brief Self-test implementations @@ -62,65 +52,55 @@ namespace allocator { */ static void test_allocator_allocates_data() { - static const size_t CAPACITY = 16; + static const size_t CAPACITY = 64; linear_allocator la{}; + // Allocate space for an int and construct at the mem with placement new auto* mem = la.allocate(sizeof(int)); - int* num_ptr = new (mem) int{69}; ///< use placement new to initialize an - ///< int at the memory we allocated + int* num_ptr = new (mem) int{69}; assert(69 == *num_ptr); - assert(4 == la.size()); - assert(16 == la.cap()); + assert(la.size() > 0); + assert(la.size() <= CAPACITY); + assert(0 == la.size() % alignof(std::max_align_t)); } static void test_allocator_resets() { - static const size_t CAPACITY = 8; + static const size_t CAPACITY = 64; linear_allocator la{}; auto* mem = la.allocate(sizeof(int)); - int* num_ptr = new (mem) int{69}; + new (mem) int{69}; + assert(la.size() > 0); la.reset(); assert(0 == la.size()); - - // Allocate memory at the same location as the first, should hand out the - // same bytes - auto* new_mem = la.allocate(sizeof(int)); - int* new_num_ptr = new (new_mem) int{10}; - - assert(10 == *new_num_ptr); - assert(*new_num_ptr == *num_ptr); } static void test_allocate_when_full_returns_nullptr() { - static const size_t CAPACITY = sizeof(double); + static const size_t CAPACITY = alignof(std::max_align_t); linear_allocator la{}; - auto* mem = la.allocate(sizeof(double)); - [[maybe_unused]] double* _ = new (mem) double{69.0}; + auto* mem = la.allocate(sizeof(char)); + assert(mem != nullptr); + new (mem) char{'A'}; - auto* some_char = la.allocate(sizeof(char)); - assert(nullptr == some_char); + assert(nullptr == la.allocate(CAPACITY)); } -static void tests() { +static void test() { test_allocator_allocates_data(); test_allocator_resets(); test_allocate_when_full_returns_nullptr(); std::cout << "All tests have successfully passed!\n"; } - /** * @brief Main function * @returns 0 on exit */ int main() { - tests(); + test(); return 0; } - -} // namespace allocator -} // namespace memory \ No newline at end of file diff --git a/memory/linear_allocator.hpp b/memory/linear_allocator.hpp index d30bb8df80..d9bac84bba 100644 --- a/memory/linear_allocator.hpp +++ b/memory/linear_allocator.hpp @@ -28,7 +28,7 @@ * std::max_align_t which ensures that fundamental types are aligned to the * largest type on the current platform. However, when trying to allocate for * complex types which may be larger, this doesn't work. One solution would be - * to take a note from * + * to take a note from * [std::pmr::memory_resource::allocate](https://en.cppreference.com/cpp/memory/memory_resource/allocate) * and have the alignment as a default parameter. * From 470bd17e27ce59036a7fd3c47cb89e0ae456e483 Mon Sep 17 00:00:00 2001 From: B33Boy Date: Mon, 27 Apr 2026 20:52:48 -0400 Subject: [PATCH 6/7] refactor: rename size to offset and update docs --- memory/linear_allocator.cpp | 25 ++++++++++++++++++------- memory/linear_allocator.hpp | 32 +++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/memory/linear_allocator.cpp b/memory/linear_allocator.cpp index e28fe008eb..4641a89c78 100644 --- a/memory/linear_allocator.cpp +++ b/memory/linear_allocator.cpp @@ -47,10 +47,9 @@ using namespace memory::allocator; /** - * @brief Self-test implementations + * @brief Tests that the allocator hands out valid aligned memory * @returns void */ - static void test_allocator_allocates_data() { static const size_t CAPACITY = 64; linear_allocator la{}; @@ -60,23 +59,31 @@ static void test_allocator_allocates_data() { int* num_ptr = new (mem) int{69}; assert(69 == *num_ptr); - assert(la.size() > 0); - assert(la.size() <= CAPACITY); - assert(0 == la.size() % alignof(std::max_align_t)); + assert(la.offset() > 0); + assert(la.offset() <= CAPACITY); + assert(0 == la.offset() % alignof(std::max_align_t)); } +/** + * @brief Tests that reset returns the offset to zero + * @returns void + */ static void test_allocator_resets() { static const size_t CAPACITY = 64; linear_allocator la{}; auto* mem = la.allocate(sizeof(int)); new (mem) int{69}; - assert(la.size() > 0); + assert(la.offset() > 0); la.reset(); - assert(0 == la.size()); + assert(0 == la.offset()); } +/** + * @brief Tests that a full allocator returns nullptr + * @returns void + */ static void test_allocate_when_full_returns_nullptr() { static const size_t CAPACITY = alignof(std::max_align_t); linear_allocator la{}; @@ -88,6 +95,10 @@ static void test_allocate_when_full_returns_nullptr() { assert(nullptr == la.allocate(CAPACITY)); } +/** + * @brief Self-test implementations + * @returns void + */ static void test() { test_allocator_allocates_data(); test_allocator_resets(); diff --git a/memory/linear_allocator.hpp b/memory/linear_allocator.hpp index d9bac84bba..db92e0e0ae 100644 --- a/memory/linear_allocator.hpp +++ b/memory/linear_allocator.hpp @@ -67,7 +67,7 @@ class linear_allocator { // ================= Special Member Functions ================= /** - * For simplicity, we will disallow copying or moving the allocator. + * For simplicity, we will disable copying or moving the allocator. */ ~linear_allocator() = default; @@ -110,21 +110,21 @@ class linear_allocator { * @brief resets allocator to the start * */ - void reset() noexcept { next_ = 0; } + constexpr void reset() noexcept { next_ = 0; } /** - * @brief returns size of data allocated + * @brief return the offset of the data allocated * - * @return size_t next position + * @return size_t offset */ - auto size() const noexcept -> size_t { return next_; } + constexpr auto offset() const noexcept -> size_t { return next_; } /** - * @brief return capacity of the allocator + * @brief return the capacity of the allocator * * @return size_t capacity */ - auto cap() const noexcept -> size_t { return N; } + constexpr auto cap() const noexcept -> size_t { return N; } private: static constexpr size_t alignment = alignof(std::max_align_t); @@ -132,6 +132,24 @@ class linear_allocator { alignas(alignment) std::byte buffer_[N]; size_t next_{0}; + /** + * @brief return the next aligned offset + * + * @details + * Given any offset, the next aligned value is a multiple of alignment (must + * be power of 2). For example, for an alignment requirement of 8, if the + * offset is in range [0,8] it returns 8, if it's in range [9,16] it + * returns 16. + * + * This works by first overshooting the offset by `alignment - 1`, then + * masking off the low bits to round down to the nearest aligned value. + * + * This trick is only valid when alignment is a power of 2 because there is + * only 1 set bit (e.g. 8 is 0x1000, and 4 is 0x0100) + * + * @param offset byte offset in a buffer + * @return size_t aligned offset + */ static constexpr auto align(size_t offset) noexcept -> size_t { return (offset + (alignment - 1)) & ~(alignment - 1); } From c27e418d45fa03ba1d51516e8c1bba94be12a405 Mon Sep 17 00:00:00 2001 From: B33Boy Date: Mon, 27 Apr 2026 21:13:19 -0400 Subject: [PATCH 7/7] fix: resolve clang-tidy errors --- memory/linear_allocator.cpp | 16 ++++++++-------- memory/linear_allocator.hpp | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/memory/linear_allocator.cpp b/memory/linear_allocator.cpp index 4641a89c78..fc90b6d687 100644 --- a/memory/linear_allocator.cpp +++ b/memory/linear_allocator.cpp @@ -44,21 +44,20 @@ #include /// for std::assert #include /// for IO operations -using namespace memory::allocator; - /** * @brief Tests that the allocator hands out valid aligned memory * @returns void */ static void test_allocator_allocates_data() { static const size_t CAPACITY = 64; - linear_allocator la{}; + memory::allocator::linear_allocator la{}; - // Allocate space for an int and construct at the mem with placement new + // Allocate space for an int auto* mem = la.allocate(sizeof(int)); - int* num_ptr = new (mem) int{69}; + assert(mem != nullptr); - assert(69 == *num_ptr); + // construct the int at the mem with placement new + assert(69 == *new (mem) int{69}); assert(la.offset() > 0); assert(la.offset() <= CAPACITY); assert(0 == la.offset() % alignof(std::max_align_t)); @@ -70,9 +69,10 @@ static void test_allocator_allocates_data() { */ static void test_allocator_resets() { static const size_t CAPACITY = 64; - linear_allocator la{}; + memory::allocator::linear_allocator la{}; auto* mem = la.allocate(sizeof(int)); + assert(mem != nullptr); new (mem) int{69}; assert(la.offset() > 0); @@ -86,7 +86,7 @@ static void test_allocator_resets() { */ static void test_allocate_when_full_returns_nullptr() { static const size_t CAPACITY = alignof(std::max_align_t); - linear_allocator la{}; + memory::allocator::linear_allocator la{}; auto* mem = la.allocate(sizeof(char)); assert(mem != nullptr); diff --git a/memory/linear_allocator.hpp b/memory/linear_allocator.hpp index db92e0e0ae..0e3844be59 100644 --- a/memory/linear_allocator.hpp +++ b/memory/linear_allocator.hpp @@ -97,8 +97,9 @@ class linear_allocator { -> std::byte* { size_t aligned_next = linear_allocator::align(next_ + num_bytes); - if (aligned_next > N) + if (aligned_next > N) { return nullptr; + } auto* mem = &buffer_[next_]; next_ = aligned_next;