Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions memory/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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} )
117 changes: 117 additions & 0 deletions memory/linear_allocator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* @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.
*
*
* 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/)
*/

#include "linear_allocator.hpp"

#include <cassert> /// for std::assert
#include <iostream> /// for IO operations

/**
* @brief Tests that the allocator hands out valid aligned memory
* @returns void
*/
static void test_allocator_allocates_data() {
static const size_t CAPACITY = 64;
memory::allocator::linear_allocator<CAPACITY> la{};

// Allocate space for an int
auto* mem = la.allocate(sizeof(int));
assert(mem != nullptr);

// 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));
}

/**
* @brief Tests that reset returns the offset to zero
* @returns void
*/
static void test_allocator_resets() {
static const size_t CAPACITY = 64;
memory::allocator::linear_allocator<CAPACITY> la{};

auto* mem = la.allocate(sizeof(int));
assert(mem != nullptr);
new (mem) int{69};
assert(la.offset() > 0);

la.reset();
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);
memory::allocator::linear_allocator<CAPACITY> la{};

auto* mem = la.allocate(sizeof(char));
assert(mem != nullptr);
new (mem) char{'A'};

assert(nullptr == la.allocate(CAPACITY));
}

/**
* @brief Self-test implementations
* @returns void
*/
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() {
test();

return 0;
}
160 changes: 160 additions & 0 deletions memory/linear_allocator.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* @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.
*
*
* 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 <cstddef> /// 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 <size_t N>
class linear_allocator {
public:
constexpr linear_allocator() = default;

// ================= Special Member Functions =================
/**
* For simplicity, we will disable 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
*
*/
constexpr void reset() noexcept { next_ = 0; }

/**
* @brief return the offset of the data allocated
*
* @return size_t offset
*/
constexpr auto offset() const noexcept -> size_t { return next_; }

/**
* @brief return the capacity of the allocator
*
* @return size_t capacity
*/
constexpr 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};

/**
* @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);
}
};

} // namespace allocator
} // namespace memory