├── .gitmodules ├── tests ├── main.cpp ├── CMakeLists.txt ├── utilities_tests.cpp ├── allocator_tests.cpp └── resource_tests.cpp ├── .gitignore ├── .github └── workflows │ └── test.yml ├── include └── realtime_memory │ ├── containers.h │ ├── pmr_includes.h │ ├── realtime_memory.h │ ├── utilities.h │ ├── polymorphic_allocators.h │ ├── free_list_resource.h │ └── memory_resources.h ├── LICENSE ├── CMakeLists.txt ├── README.md └── CODE_OF_CONDUCT.md /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/main.cpp: -------------------------------------------------------------------------------- 1 | #define CONFIG_CATCH_MAIN 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build-debug 2 | build-release 3 | cmake-build-debug 4 | cmake-build-release 5 | compile-commands.json -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Run unit tests using CTest 2 | name: Tests 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | run-tests: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Generate CMake 16 | run: mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Release .. 17 | - name: Build project 18 | run: cd build && cmake --build . 19 | - name: Run tests 20 | run: cd build && ctest . --verbose 21 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | #------------------------------------------------------------------ 4 | # Tests 5 | #------------------------------------------------------------------ 6 | Include(FetchContent) 7 | FetchContent_Declare(Catch2 8 | GIT_REPOSITORY https://github.com/catchorg/Catch2.git 9 | GIT_TAG v3.11.0) # or a later release 10 | FetchContent_MakeAvailable(Catch2) 11 | 12 | add_executable(realtime_memory_tests 13 | main.cpp 14 | resource_tests.cpp; 15 | allocator_tests.cpp; 16 | utilities_tests.cpp) 17 | target_link_libraries(realtime_memory_tests PRIVATE 18 | realtime_memory 19 | Catch2::Catch2WithMain) 20 | 21 | if(MSVC) 22 | target_compile_options(realtime_memory_tests PRIVATE /W4 /WX) 23 | else() 24 | target_compile_options(realtime_memory_tests PRIVATE -Wall -Wextra -Wpedantic -Werror) 25 | endif() 26 | -------------------------------------------------------------------------------- /include/realtime_memory/containers.h: -------------------------------------------------------------------------------- 1 | //============================================================================== 2 | // Copyright (c) 2019-2022 CradleApps, LLC - All Rights Reserved 3 | //============================================================================== 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "polymorphic_allocators.h" 9 | 10 | namespace cradle::pmr 11 | { 12 | template 13 | using vector = std::vector>; 14 | 15 | template 16 | using basic_string = std::basic_string, propagating_allocator>; 17 | using string = basic_string; 18 | 19 | template 20 | using map = std::map>; 21 | 22 | template 23 | using unordered_map = std::unordered_map>; 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 CradleApps, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /include/realtime_memory/pmr_includes.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #if defined (__apple_build_version__) 9 | #define USE_EXPERIMENTAL_PMR __apple_build_version__ < 15000000 || __MAC_OS_X_VERSION_MIN_REQUIRED < 140000 10 | #elif defined (__clang__) 11 | #define USE_EXPERIMENTAL_PMR _LIBCPP_VERSION < 1600 12 | #else 13 | #define USE_EXPERIMENTAL_PMR 0 14 | #endif 15 | 16 | #if USE_EXPERIMENTAL_PMR 17 | #include 18 | namespace std_pmr = std::experimental::pmr; 19 | #else 20 | #include 21 | namespace std_pmr = std::pmr; 22 | #endif 23 | 24 | #if defined (__apple_build_version__) && USE_EXPERIMENTAL_PMR 25 | #define PMR_DIAGNOSTIC_PUSH \ 26 | _Pragma("clang diagnostic push") \ 27 | _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") 28 | #define PMR_DIAGNOSTIC_POP \ 29 | _Pragma("clang diagnostic pop") 30 | #else 31 | #define PMR_DIAGNOSTIC_PUSH 32 | #define PMR_DIAGNOSTIC_POP 33 | #endif 34 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | project("realtime_memory" VERSION 1.0.0 4 | DESCRIPTION "A header-only backfill for some C++17 pmr memory resources suitable for realtime use." 5 | HOMEPAGE_URL "https://github.com/cradleapps/memory_resources") 6 | 7 | option(REALTIME_MEMORY_TESTS "Enable build of tests" ON) 8 | 9 | #------------------------------------------------------------------ 10 | # Header-only library 11 | #------------------------------------------------------------------ 12 | add_library(realtime_memory INTERFACE) 13 | target_include_directories(realtime_memory INTERFACE 14 | $) 15 | target_compile_features(realtime_memory INTERFACE cxx_std_17) 16 | 17 | #------------------------------------------------------------------ 18 | # Tests 19 | #------------------------------------------------------------------ 20 | if (REALTIME_MEMORY_TESTS) 21 | message(STATUS "realtime_memory adding tests...") 22 | enable_testing() 23 | add_subdirectory(tests "${CMAKE_BINARY_DIR}/tests") 24 | add_test(realtime_memory tests/realtime_memory_tests) 25 | endif() 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Purpose 2 | 3 | At time of writing (Nov 2022) clang's libc++ has still not implemented 4 | the concrete `memory_resource` types defined by the standard in the 5 | `std::pmr` namespace. 6 | 7 | We have found a need for these in our development of realtime audio 8 | software. This library offers the `memory_resource` types we have 9 | found useful for realtime software, plus one extra resource that 10 | is not offered in the standard (the `free_list_resource`). 11 | 12 | | Type name | cradle::pmr | std::pmr | std::pmr (libc++) | 13 | | ---------------------------- | ------------- | ------------- | ----------------- | 14 | | monotonic_buffer_resource | Yes | Yes | No | 15 | | unsynchronized_pool_resource | Yes | Yes | No | 16 | | synchronized_pool_resource | No | Yes | No | 17 | | new_delete_resource | Yes | Yes | No | 18 | | null_memory_resource | Yes | Yes | No | 19 | | free_list_resource | Yes | No | No | 20 | 21 | The library is licensed under the **MIT license**. 22 | -------------------------------------------------------------------------------- /include/realtime_memory/realtime_memory.h: -------------------------------------------------------------------------------- 1 | //============================================================================== 2 | // Copyright (c) 2019-2022 CradleApps, LLC - All Rights Reserved 3 | //============================================================================== 4 | /** A fill-in implementation of C++17 , to get 5 | * monotonic_buffer_resource and unsynchronized_pool_resource. 6 | * 7 | * Sadly these haven't made it to libc++ (clang) yet, though MSVC 8 | * and newer GCCs have got implementations. This file is intended 9 | * to be a drop-in replacement, and it is inspired by the MSVC 10 | * and Bloomberg implementations (permissive licenses). 11 | * 12 | * Example: this is a pool-based allocator that sources its memory 13 | * from a pre-allocated buffer, and never calls malloc/new/delete. 14 | * This would be suitable for use on a real-time thread, but not from 15 | * multiple threads at once - the unsynchronized_pool is not threadsafe. 16 | * If the buffer is exhausted, any allocation will throw std::bad_alloc. 17 | * myVec is a vector which uses this allocator as a memory source. 18 | * myVec2 is the same kind of vector, but uses a more convenient syntax. 19 | * 20 | * using namespace cradle::pmr; 21 | * auto b = std::vector (2048, std:byte (0)); 22 | * auto m = monotonic_buffer_resource (b.data(), b.size(), null_memory_resource()); 23 | * auto pool = unsynchronized_pool_resource (&m); 24 | * 25 | * auto myVec = std::vector> (&pool); 26 | * auto myVec2 = cradle::pmr::vector (&pool); 27 | * 28 | * Note: if many allocations exceed pool_options::largest_required_pool_block 29 | * and they need to be freed later, this set-up will waste a lot of memory. 30 | * It's because the pool defers 'oversized' allocations to its upstream source. 31 | * In this case that's a monotonic_buffer_resource, which cannot free memory. 32 | */ 33 | #include "memory_resources.h" 34 | #include "polymorphic_allocators.h" 35 | #include "containers.h" 36 | -------------------------------------------------------------------------------- /tests/utilities_tests.cpp: -------------------------------------------------------------------------------- 1 | //============================================================================== 2 | // Copyright (c) 2019-2023 CradleApps, LLC - All Rights Reserved 3 | //============================================================================== 4 | 5 | #include 6 | #include "realtime_memory/utilities.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | //============================================================================== 15 | TEST_CASE ("merge sort linked lists") 16 | { 17 | struct node 18 | { 19 | node* next = nullptr; 20 | int data = 0; 21 | 22 | explicit node (int d) : data (d) {} 23 | }; 24 | 25 | const int listTailLength = 1000; 26 | auto rand = Catch::Generators::random (0, 100000); 27 | 28 | // Build the list 29 | auto head = new node (rand.get()); 30 | auto current = head; 31 | 32 | auto deleter = std::vector>(); 33 | deleter.emplace_back (head); 34 | 35 | for (int i = 0; i < listTailLength; ++i, rand.next()) 36 | { 37 | current->next = new node (rand.get()); 38 | current = current->next; 39 | deleter.emplace_back (current); 40 | } 41 | 42 | const auto lowestFirst = [] (node* a, node* b) { return a->data < b->data; }; 43 | const auto highestFirst = [] (node* a, node* b) { return a->data > b->data; }; 44 | 45 | SECTION ("Sort lowest first") 46 | { 47 | head = cradle::pmr::merge_sort_list (head, lowestFirst); 48 | 49 | int numListPairs = 0; 50 | int numSortedPairs = 0; 51 | 52 | for (auto n = head; n != nullptr && n->next != nullptr; n = n->next) 53 | { 54 | numSortedPairs += n->data <= n->next->data ? 1 : 0; 55 | ++numListPairs; 56 | } 57 | 58 | CHECK (numListPairs == listTailLength); 59 | CHECK (numSortedPairs == listTailLength); 60 | } 61 | 62 | SECTION ("Sort highest first") 63 | { 64 | head = cradle::pmr::merge_sort_list (head, highestFirst); 65 | 66 | int numListPairs = 0; 67 | int numSortedPairs = 0; 68 | 69 | for (auto n = head; n != nullptr && n->next != nullptr; n = n->next) 70 | { 71 | numSortedPairs += n->data >= n->next->data ? 1 : 0; 72 | ++numListPairs; 73 | } 74 | 75 | CHECK (numListPairs == listTailLength); 76 | CHECK (numSortedPairs == listTailLength); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /include/realtime_memory/utilities.h: -------------------------------------------------------------------------------- 1 | //============================================================================== 2 | // Copyright (c) 2019-2023 CradleApps, LLC - All Rights Reserved 3 | //============================================================================== 4 | #pragma once 5 | #include 6 | 7 | namespace cradle::pmr 8 | { 9 | 10 | /** Merge sort a singly linked list. 11 | * - `list_node` must have a `next` pointer. 12 | * - `compare_nodes_fn` should take two `list_node` pointers and 13 | * return true if the first should be before the second in the list. 14 | * 15 | * This is probably the fastest way of sorting a singly linked list 16 | * without allocating auxiliary memory. 17 | * 18 | * Inspired by this public-domain pseudocode: 19 | * https://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html 20 | */ 21 | template 22 | list_node* merge_sort_list (list_node* list, compare_nodes_fn&& compare) 23 | { 24 | if (list == nullptr) 25 | return nullptr; 26 | 27 | int chunk_size = 1; 28 | 29 | while (true) 30 | { 31 | list_node* p = std::exchange (list, nullptr); 32 | list_node* tail = nullptr; 33 | int num_merges = 0; 34 | 35 | while (p != nullptr) 36 | { 37 | ++num_merges; 38 | 39 | list_node* q = p; 40 | int q_size = chunk_size; 41 | int p_size = 0; 42 | 43 | for (int i = 0; i < chunk_size; ++i) 44 | { 45 | ++p_size; 46 | q = q->next; 47 | 48 | if (q == nullptr) 49 | break; 50 | } 51 | 52 | list_node* next = nullptr; 53 | 54 | // merge the two lists, choosing nodes from p or q as appropriate. 55 | while (p_size > 0 || (q_size > 0 && q != nullptr)) 56 | { 57 | if (p_size != 0 && (q_size == 0 || q == nullptr || compare (p, q))) 58 | { 59 | next = p; 60 | p = p->next; 61 | --p_size; 62 | } 63 | else 64 | { 65 | next = q; 66 | q = q->next; 67 | --q_size; 68 | } 69 | 70 | // add the next node to the merged list 71 | if (tail == nullptr) 72 | list = next; 73 | else 74 | tail->next = next; 75 | 76 | tail = next; 77 | } 78 | 79 | // both p and q have stepped `chunk_size' places along 80 | p = q; 81 | } 82 | 83 | tail->next = nullptr; 84 | 85 | // If we have done only one merge, we're finished. 86 | if (num_merges <= 1) // could be 0 if p is empty 87 | return list; 88 | 89 | // Otherwise repeat, merging lists twice the size 90 | chunk_size *= 2; 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /tests/allocator_tests.cpp: -------------------------------------------------------------------------------- 1 | //============================================================================== 2 | // Copyright (c) 2019-2023 CradleApps, LLC - All Rights Reserved 3 | //============================================================================== 4 | 5 | #include 6 | #include "realtime_memory/memory_resources.h" 7 | #include "realtime_memory/polymorphic_allocators.h" 8 | #include "realtime_memory/containers.h" 9 | 10 | TEST_CASE ("Allocator propagated with string copy construction", "[RealtimeMemory]") 11 | { 12 | cradle::pmr::monotonic_buffer_resource resource (8192); 13 | cradle::pmr::propagating_allocator allocator (&resource); 14 | cradle::pmr::string s1 (allocator); 15 | 16 | cradle::pmr::string s2 (s1); 17 | 18 | CHECK (s2.get_allocator() == allocator); 19 | } 20 | 21 | TEST_CASE ("Allocator moved with string move construction", "[RealtimeMemory]") 22 | { 23 | cradle::pmr::monotonic_buffer_resource resource (8192); 24 | cradle::pmr::propagating_allocator allocator (&resource); 25 | cradle::pmr::string s1 (allocator); 26 | 27 | cradle::pmr::string s2 (std::move (s1)); 28 | 29 | CHECK (s2.get_allocator() == allocator); 30 | } 31 | 32 | TEST_CASE ("Allocator propagated with string assignment", "[RealtimeMemory]") 33 | { 34 | cradle::pmr::monotonic_buffer_resource resource (8192); 35 | cradle::pmr::propagating_allocator allocator (&resource); 36 | cradle::pmr::string s1 ("uses allocator", allocator); 37 | cradle::pmr::string s2 ("system allocator", cradle::pmr::propagating_allocator (cradle::pmr::new_delete_resource())); 38 | 39 | CAPTURE (s2); // prevent optimisation away 40 | REQUIRE (s2.get_allocator() != allocator); 41 | 42 | s2 = s1; 43 | 44 | CHECK (s2.get_allocator() == allocator); 45 | } 46 | 47 | TEST_CASE ("Allocator propagated with string move assignment", "[RealtimeMemory]") 48 | { 49 | cradle::pmr::monotonic_buffer_resource resource (8192); 50 | cradle::pmr::propagating_allocator allocator (&resource); 51 | cradle::pmr::string s1 ("uses allocator", allocator); 52 | cradle::pmr::string s2 ("system allocator", cradle::pmr::propagating_allocator (cradle::pmr::new_delete_resource())); 53 | 54 | CAPTURE (s2); // prevent optimisation away 55 | REQUIRE (s2.get_allocator() != allocator); 56 | 57 | s2 = std::move (s1); 58 | 59 | CHECK (s2.get_allocator() == allocator); 60 | } 61 | 62 | TEST_CASE ("Allocator propagated with vector copy construction", "[RealtimeMemory]") 63 | { 64 | cradle::pmr::monotonic_buffer_resource resource (8192); 65 | cradle::pmr::propagating_allocator allocator (&resource); 66 | cradle::pmr::vector v1 (allocator); 67 | 68 | cradle::pmr::vector v2 (v1); 69 | 70 | CHECK (v2.get_allocator() == allocator); 71 | } 72 | 73 | TEST_CASE ("Allocator propagated with nested container copy construction", "[RealtimeMemory]") 74 | { 75 | cradle::pmr::monotonic_buffer_resource resource (8192); 76 | cradle::pmr::propagating_allocator allocator (&resource); 77 | cradle::pmr::vector v1 (allocator); 78 | v1.emplace_back ("hello", allocator); 79 | REQUIRE (v1.size() == 1); 80 | REQUIRE (v1[0].get_allocator() == allocator); 81 | 82 | cradle::pmr::vector v2 (v1); 83 | 84 | REQUIRE (v2.size() == 1); 85 | CHECK (v2[0].get_allocator() == allocator); 86 | } 87 | -------------------------------------------------------------------------------- /include/realtime_memory/polymorphic_allocators.h: -------------------------------------------------------------------------------- 1 | //============================================================================== 2 | // Copyright (c) 2019-2022 CradleApps, LLC - All Rights Reserved 3 | //============================================================================== 4 | 5 | #pragma once 6 | #include "pmr_includes.h" 7 | #include "memory_resources.h" 8 | 9 | PMR_DIAGNOSTIC_PUSH 10 | 11 | namespace cradle::pmr 12 | { 13 | /** A simple alias would trigger 'undefined symbols for get_default_resource()' on ARM. */ 14 | template 15 | class polymorphic_allocator : public std_pmr::polymorphic_allocator 16 | { 17 | public: 18 | polymorphic_allocator() : base (get_default_resource()) {} 19 | polymorphic_allocator (memory_resource* r) : base (r) {} 20 | polymorphic_allocator (polymorphic_allocator& other) : base (other) {} 21 | 22 | template 23 | polymorphic_allocator (const polymorphic_allocator& other) noexcept 24 | : base (other.resource()) 25 | {} 26 | 27 | using base = std_pmr::polymorphic_allocator; 28 | }; 29 | 30 | /** propagating_allocator is a polymorphic allocator similar to polymorphic_allocator 31 | * but which propagates when containers (strings, vectors, etc.) are copied and moved. 32 | * 33 | * It is designed to be used in a realtime context, when we must ensure that all 34 | * object allocations are from a non-locking memory resource. 35 | * 36 | * The container aliases defined in use this allocator type. 37 | */ 38 | template 39 | class propagating_allocator 40 | { 41 | public: 42 | using value_type = ValueType; 43 | 44 | propagating_allocator (cradle::pmr::memory_resource* _resource) 45 | : m_resource (_resource) 46 | { 47 | } 48 | 49 | propagating_allocator (const propagating_allocator&) = default; 50 | 51 | template 52 | propagating_allocator (const propagating_allocator& other) noexcept 53 | : m_resource (other.resource()) 54 | { 55 | } 56 | 57 | propagating_allocator& operator= (const propagating_allocator& other) 58 | { 59 | this->m_resource = other.m_resource; 60 | return *this; 61 | } 62 | 63 | propagating_allocator select_on_container_copy_construction() const 64 | { 65 | return *this; 66 | } 67 | 68 | ValueType* allocate (const size_t count) 69 | { 70 | void* ptr = m_resource->allocate (sizeof (ValueType) * (count), alignof (ValueType)); 71 | return static_cast (ptr); 72 | } 73 | 74 | void deallocate (ValueType* ptr, const size_t count) 75 | { 76 | m_resource->deallocate (ptr, count * sizeof (ValueType), alignof (ValueType)); 77 | } 78 | 79 | using propagate_on_container_copy_assignment = std::true_type; 80 | using propagate_on_container_move_assignment = std::true_type; 81 | using propagate_on_container_swap = std::true_type; 82 | 83 | template 84 | void construct (OtherType* ptr, ArgTypes&&... _Args) 85 | { 86 | cradle::pmr::polymorphic_allocator alloc (m_resource); 87 | alloc.construct (ptr, std::forward (_Args)...); 88 | } 89 | 90 | template 91 | void destroy (OtherType* ptr) 92 | { 93 | ptr->~OtherType(); 94 | } 95 | 96 | cradle::pmr::memory_resource* resource() const 97 | { 98 | return m_resource; 99 | } 100 | 101 | private: 102 | cradle::pmr::memory_resource* m_resource = cradle::pmr::get_default_resource(); 103 | }; 104 | 105 | template 106 | bool operator== (const propagating_allocator& alloc1, 107 | const propagating_allocator& alloc2) noexcept 108 | { 109 | return alloc1.resource()->is_equal (*alloc2.resource()); 110 | } 111 | 112 | template 113 | bool operator!= (const propagating_allocator& alloc1, 114 | const propagating_allocator& alloc2) noexcept 115 | { 116 | return ! (alloc1 == alloc2); 117 | } 118 | 119 | } // cradle::pmr 120 | 121 | PMR_DIAGNOSTIC_POP 122 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at opensource@cradle.app. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | 134 | -------------------------------------------------------------------------------- /include/realtime_memory/free_list_resource.h: -------------------------------------------------------------------------------- 1 | //============================================================================== 2 | // Copyright (c) 2019-2022 CradleApps, LLC - All Rights Reserved 3 | //============================================================================== 4 | /** A std::pmr compatible memory_resource that can allocate and 5 | * free blocks of any size. It can be supplied from an upstream 6 | * resource, a fixed buffer, or a series of fixed buffers pushed 7 | * in by an external routine (to allow async allocation techniques). 8 | * 9 | * Important notes: 10 | * - This resource is single-threaded 11 | * - This resource does not support over-alignment. 12 | * 13 | * If we need these, we may add them, but for now YAGNI. 14 | * 15 | * It's a first-fit free-list algorithm, which is relatively 16 | * slow. Thus it's best used for large but rare allocations. 17 | * 18 | * A good technique is to use it as the upstream resource for 19 | * an unsynchronized_pool_resource. Small allocations will be 20 | * served by the pool (which is fast) while oversize 21 | * allocations (which are rarer) will be served by this object. 22 | * This object would also provide the memory for the pool itself. 23 | */ 24 | #pragma once 25 | #include 26 | #include 27 | 28 | namespace cradle::pmr 29 | { 30 | //============================================================================== 31 | class free_list_resource : public cradle::pmr::identity_equal_resource 32 | { 33 | public: 34 | /** Allocate from a buffer. Allocations throw when this runs out. */ 35 | free_list_resource (std::byte* buffer, std::size_t buffer_size) 36 | { 37 | expand (buffer, buffer_size); 38 | } 39 | 40 | /** Allocate from an upstream memory resource. 41 | * 42 | * The min_realloc_size describes the minimum size of the memory 43 | * chunk requested from the upstream when the current one runs out. 44 | */ 45 | free_list_resource (memory_resource& upstream_resource, 46 | std::size_t initial_size, 47 | std::size_t min_chunk_size = std::size_t (64 * 1024)) 48 | : upstream (&upstream_resource), 49 | min_realloc_size (min_chunk_size) 50 | { 51 | expand (upstream->allocate (initial_size, 1), initial_size); 52 | } 53 | 54 | /** Add more memory that can be used for allocations. 55 | * If you supplied a capable upstream_resource constructor, you don't 56 | * need to use this and more memory will be requested from the upstream 57 | * resource if needed. 58 | * 59 | * However, if you are working in a domain where you can allocate/replenish 60 | * memory at certain times and not others, this allows you to do that. 61 | */ 62 | void expand (void* new_memory, std::size_t new_memory_size) 63 | { 64 | if (new_memory == nullptr || new_memory_size < sizeof(mem_block)) 65 | throw std::bad_alloc(); 66 | 67 | auto pos = align_block (new_memory, new_memory_size); 68 | space += new_memory_size; 69 | first_free = ::new (pos) mem_block (first_free, new_memory_size - mem_block::header_size()); 70 | } 71 | 72 | /** Returns the remaining space in the buffer, in bytes. */ 73 | std::size_t available_space() const { return space; } 74 | 75 | /** Returns the number of individual allocations that have been made. */ 76 | std::size_t num_allocations() const { return num_allocs; } 77 | 78 | //============================================================================== 79 | /** You will interact with the resource via the public functions of its base-class. */ 80 | private: 81 | struct mem_block; // forward-declare. 82 | 83 | void* do_allocate (std::size_t bytes, std::size_t align) override 84 | { 85 | if (bytes == 0) 86 | throw std::bad_alloc(); 87 | if (align > max_align_bytes) 88 | throw std::bad_alloc(); // Over-alignment is not currently supported! 89 | 90 | const auto size = cradle::pmr::detail::aligned_ceil (bytes, std::min (align, max_align_bytes)); 91 | const auto next_chunk_size = std::max (min_realloc_size, size + mem_block::header_size()); 92 | 93 | if ((size + mem_block::header_size()) >= space) 94 | expand (upstream->allocate (next_chunk_size, max_align_bytes), next_chunk_size); 95 | 96 | for (mem_block* free = first_free, *prev = nullptr; free != nullptr; prev = free, free = free->next) 97 | if (free->size >= size) 98 | return assign_block (*free, prev, size); 99 | 100 | // Too fragmented: we have enough space but no blocks are large enough. 101 | if (auto defragmented_block = defragment (size)) 102 | return defragmented_block; 103 | 104 | // Still too fragmented: we'll have to allocate another chunk. 105 | expand (upstream->allocate (next_chunk_size, max_align_bytes), next_chunk_size); 106 | return do_allocate (bytes, align); 107 | } 108 | 109 | void do_deallocate (void* ptr, std::size_t, std::size_t) override 110 | { 111 | const auto last_first_free = std::exchange (first_free, mem_block::get_header (ptr)); 112 | first_free->next = last_first_free; 113 | space += (mem_block::header_size() + first_free->size); 114 | --num_allocs; 115 | } 116 | 117 | void* assign_block (mem_block& block, mem_block* previous_in_free_list, std::size_t required_size) 118 | { 119 | // _only split the block if we're going to save a significant number of bytes. 120 | if ((block.size - required_size) > (mem_block::header_size() + max_align_bytes * 2)) 121 | remove_from_free_list (block, previous_in_free_list, split_block (block, required_size)); 122 | else 123 | remove_from_free_list (block, previous_in_free_list, block.next); 124 | 125 | block.next = nullptr; 126 | space -= (mem_block::header_size() + block.size); 127 | ++num_allocs; 128 | return block.data(); 129 | } 130 | 131 | static mem_block* split_block (mem_block& block, std::size_t required_size) 132 | { 133 | // The block is not large enough to be split into a new block with the required size. 134 | assert (block.size - required_size > mem_block::header_size()); 135 | 136 | auto old_block_data = block.data(); 137 | auto new_block_pos = (void*) (old_block_data + required_size); 138 | auto new_block_size = (block.size - required_size) - mem_block::header_size(); 139 | auto new_block_pos_aligned = align_block (new_block_pos, new_block_size); 140 | auto new_block = ::new (new_block_pos_aligned) mem_block (block.next, new_block_size); 141 | 142 | block.size = std::size_t ((std::byte*) new_block_pos_aligned - old_block_data); 143 | return new_block; 144 | } 145 | 146 | void remove_from_free_list (mem_block& block_being_removed, mem_block* previous_in_list, mem_block* next_in_list) 147 | { 148 | if (&block_being_removed == first_free) 149 | first_free = next_in_list; 150 | else if (previous_in_list != nullptr) 151 | previous_in_list->next = next_in_list; 152 | } 153 | 154 | // Merges contiguous free blocks, and returns the block that's 155 | // closest in size to the the desired block size. 156 | void* defragment (std::size_t desired_block_size) 157 | { 158 | // First we sort the free list so that it's ordered by pointer position (lowest first) 159 | first_free = merge_sort_list (first_free, [] (mem_block* a, mem_block* b) { 160 | return reinterpret_cast (a) < reinterpret_cast (b); 161 | }); 162 | 163 | // Now we iterate the free blocks, merging the next block along if it's also free. 164 | // While we do this, we look for a block that is suitable for use by the caller. 165 | std::size_t closest_size_diff = std::numeric_limits::max(); 166 | mem_block* closest_sized_block = nullptr; 167 | mem_block* closest_sized_block_prev = nullptr; 168 | 169 | for (mem_block* free = first_free, *prev = nullptr; free != nullptr;) 170 | { 171 | // merging next block along if it's free 172 | while (free->next) 173 | { 174 | auto next = free->next; 175 | auto thisEnd = free->data() + free->size; 176 | auto nextHeader = reinterpret_cast (next); 177 | 178 | if (nextHeader - thisEnd > std::ptrdiff_t (max_align_bytes)) 179 | break; 180 | 181 | auto nextEnd = next->data() + next->size; 182 | free->size = std::size_t (nextEnd - free->data()); 183 | free->next = next->next; 184 | } 185 | 186 | // look for a block that is suitable for use by the caller 187 | if (free->size >= desired_block_size) 188 | { 189 | const auto diff = free->size - desired_block_size; 190 | 191 | if (diff < closest_size_diff) 192 | { 193 | closest_size_diff = diff; 194 | closest_sized_block = free; 195 | closest_sized_block_prev = prev; 196 | } 197 | } 198 | 199 | prev = free; 200 | free = free->next; 201 | } 202 | 203 | if (closest_sized_block == nullptr) 204 | return nullptr; 205 | 206 | return assign_block (*closest_sized_block, closest_sized_block_prev, desired_block_size); 207 | } 208 | 209 | //============================================================================== 210 | struct mem_block 211 | { 212 | mem_block (mem_block* nx, std::size_t sz) 213 | : next (nx), size (sz) {} 214 | 215 | mem_block* next = nullptr; 216 | std::size_t size = 0; 217 | 218 | std::byte* data() 219 | { 220 | return (std::byte*) this + header_size(); 221 | } 222 | 223 | static mem_block* get_header (void* ptr) 224 | { 225 | return reinterpret_cast ((std::byte*) ptr - header_size()); 226 | } 227 | 228 | static constexpr std::size_t header_size() 229 | { 230 | return cradle::pmr::detail::aligned_ceil (sizeof (mem_block), max_align_bytes); 231 | } 232 | }; 233 | 234 | // Works like std::align - the input parameters are mutated, and the aligned pointer 235 | // is also returned. If the alignment can't be obtained, the input parameters are 236 | // restored to their originals, and nullptr is returned (that shouldn't ever happen). 237 | static void* align_block (void*& buffer, std::size_t& space) 238 | { 239 | static constexpr auto base_align = alignof (mem_block); 240 | static_assert (mem_block::header_size() % base_align == 0); // sanity check 241 | 242 | auto* ptr = (std::byte*) buffer; 243 | const auto* end = ptr + space; 244 | 245 | if (auto offset = std::size_t (ptr) & (base_align - 1); offset != 0) 246 | ptr += base_align - offset; 247 | 248 | while (! is_byte_aligned (ptr + mem_block::header_size())) 249 | ptr += base_align; 250 | 251 | if (ptr >= end) 252 | throw std::logic_error ("free_list_resource failed to align block"); 253 | 254 | space -= std::size_t (ptr - (std::byte*) buffer); 255 | buffer = ptr; 256 | return ptr; 257 | } 258 | 259 | template 260 | static bool is_byte_aligned (const void* address) noexcept 261 | { 262 | static_assert (byte_alignment > 0); 263 | 264 | const auto p = reinterpret_cast (address); 265 | return p % byte_alignment == 0; 266 | } 267 | 268 | static constexpr auto max_align_bytes = alignof (std::max_align_t); 269 | 270 | //============================================================================== 271 | memory_resource* upstream = null_memory_resource(); 272 | mem_block* first_free = nullptr; 273 | std::size_t space = 0; 274 | std::size_t num_allocs = 0; 275 | std::size_t min_realloc_size = 0; 276 | }; 277 | 278 | } // namespace cradle 279 | -------------------------------------------------------------------------------- /include/realtime_memory/memory_resources.h: -------------------------------------------------------------------------------- 1 | //============================================================================== 2 | // Copyright (c) 2019-2022 CradleApps, LLC - All Rights Reserved 3 | //============================================================================== 4 | #pragma once 5 | #include "pmr_includes.h" 6 | 7 | PMR_DIAGNOSTIC_PUSH 8 | 9 | //============================================================================== 10 | namespace cradle::pmr 11 | { 12 | /** Glue so we can easily switch std::pmr with std::pmr in future. */ 13 | using memory_resource = std_pmr::memory_resource; 14 | 15 | /** Reimplemented because their definition is missing on ARM! */ 16 | memory_resource* get_default_resource() noexcept; 17 | memory_resource* set_default_resource (memory_resource* r) noexcept; 18 | memory_resource* null_memory_resource() noexcept; 19 | memory_resource* new_delete_resource() noexcept; 20 | 21 | //============================================================================== 22 | /** The resource types defined in this file. */ 23 | class identity_equal_resource : public memory_resource 24 | { 25 | public: 26 | bool do_is_equal (const memory_resource& x) const noexcept override { return this == &x; } 27 | 28 | identity_equal_resource() = default; 29 | identity_equal_resource (const identity_equal_resource&) = delete; 30 | identity_equal_resource& operator= (const identity_equal_resource&) = delete; 31 | }; 32 | 33 | class null_resource : public identity_equal_resource 34 | { 35 | void* do_allocate (size_t, size_t) override { throw std::bad_alloc(); } 36 | void do_deallocate (void*, size_t, size_t) override {} 37 | }; 38 | 39 | class aligned_new_delete_resource : public identity_equal_resource 40 | { 41 | void* do_allocate (size_t bytes, size_t align) override { return ::operator new (bytes, std::align_val_t (align)); } 42 | void do_deallocate (void* ptr, size_t bytes, size_t align) override { ::operator delete (ptr, bytes, std::align_val_t (align)); } 43 | }; 44 | 45 | class monotonic_buffer_resource; // forward declare 46 | class unsynchronized_pool_resource; // forward declare 47 | struct pool_options; // forward declare 48 | 49 | //============================================================================== 50 | /** Implementation details. 51 | * 52 | * Notes on terminology: 53 | * - a block is a single slot of memory suitable for an object (e.g. 8 bytes for a uint64_t) 54 | * - a chunk is a contiguous region of memory 55 | * - a pool is a group of chunks which contain uniformly-sized blocks 56 | */ 57 | namespace detail 58 | { 59 | /** Round n up to a multiple of alignment, which must be a power of two. */ 60 | constexpr std::size_t aligned_ceil (size_t size, size_t align) noexcept; 61 | constexpr auto max_alignment = alignof(std::max_align_t); 62 | 63 | /** A list of variable-sized memory chunks, sourced from a memory_resource. */ 64 | struct chunk_list 65 | { 66 | void* allocate (std::size_t num_bytes, std::size_t align, memory_resource& upstream); 67 | void release (memory_resource& upstream); 68 | void deallocate_chunk (void* address, memory_resource& upstream); 69 | 70 | private: 71 | struct chunk 72 | { 73 | chunk* next = nullptr; 74 | chunk* prev = nullptr; 75 | std::byte* ptr = nullptr; 76 | std::uint32_t size = 0; 77 | std::uint32_t align = 0; 78 | }; 79 | 80 | chunk* first = nullptr; 81 | constexpr std::size_t header_size(); 82 | }; 83 | 84 | /** An expandable pool that can distribute memory blocks of a uniform size. */ 85 | struct pool 86 | { 87 | using block_size_t = std::uint32_t; 88 | 89 | pool (memory_resource&, 90 | block_size_t block_size_in_bytes); // throws if < min_block_size 91 | 92 | void* allocate (const pool_options&); 93 | void deallocate (void* block); 94 | void release(); 95 | 96 | block_size_t get_block_size() const { return block_size; } 97 | 98 | private: 99 | struct chunk 100 | { 101 | explicit chunk (block_size_t n, std::byte* b) 102 | : block_buffer (b), num_blocks (n) {} 103 | 104 | std::byte* block_buffer = nullptr; 105 | block_size_t num_blocks = 0; 106 | block_size_t num_initialized = 0; 107 | 108 | inline std::byte* address (block_size_t index, block_size_t block_size) const; 109 | }; 110 | 111 | struct free_link 112 | { 113 | explicit free_link (free_link* nxt) : next (nxt) {} 114 | free_link* next = nullptr; 115 | }; 116 | 117 | chunk& find_or_alloc_chunk_with_space (const pool_options&); 118 | 119 | std::vector> chunks; 120 | free_link* next_free = nullptr; 121 | block_size_t block_size; 122 | 123 | public: 124 | static constexpr auto min_block_size = block_size_t (std::max (sizeof(free_link), max_alignment)); 125 | }; 126 | } 127 | 128 | //============================================================================== 129 | /** A memory_resource implementation that allocates from a buffer 130 | * and doesn't really deallocate the memory until it is destructed. 131 | * This concept is sometimes known as a linear (or bump) allocator. 132 | * This class can be constructed with or without an initial buffer 133 | * and will always have an "upstream" memory_resource instance from 134 | * which instances will request additional memory when the current 135 | * buffer is exhausted. 136 | * 137 | * - Deallocation is a no-op. Memory is not reused, but is freed on destruct. 138 | * - Allocation is not thread-safe but is very fast while space remains 139 | * in the buffer and the upstream resource is not used. 140 | * - Individual allocations may have any size up to the provided 141 | * buffer's size while it has space. After this, allocations 142 | * must be limited to 4GB in size. 143 | */ 144 | class monotonic_buffer_resource : public identity_equal_resource 145 | { 146 | public: 147 | /** Create a monotonic_buffer_resource with no initial buffer, that will 148 | * use the result of pmr::get_default_resource() as its upstream 149 | * memory_resource. 150 | */ 151 | monotonic_buffer_resource() noexcept; 152 | 153 | /** Creates a monotonic_buffer_resource that will used the supplied 154 | * buffer as its initial buffer. Upon exhaustion of this initial 155 | * buffer, further memory will be allocated from the result of calling 156 | * pmr::get_upstream_resource() 157 | */ 158 | monotonic_buffer_resource (void* buffer, std::size_t buffer_size) noexcept; 159 | 160 | /** Creates a monotonic_buffer_resouce that will use the supplied 161 | * buffer as its initial buffer and will allocate from the supplied 162 | * upstream resource upon exhaustion of the initial buffer. 163 | */ 164 | monotonic_buffer_resource (void* buffer, 165 | std::size_t buffer_size, 166 | memory_resource* upstream) noexcept; 167 | 168 | /** Creates a monotonic_buffer_resource that uses the supplied 169 | * memory_resource as its upstream resource. 170 | */ 171 | explicit monotonic_buffer_resource (memory_resource* upstream) noexcept; 172 | 173 | /** Creates a monotonic_buffer_resource that will use the larger of an 174 | * internal initial buffer size and the buffer size specified here 175 | * as the size of the initial buffer requested from the upstream 176 | * resource. In this case since upstream is unspecified, upstream will 177 | * be the result of calling pmr::get_default_resource() 178 | */ 179 | explicit monotonic_buffer_resource (std::size_t initial_size) noexcept; 180 | 181 | /** Creates a monotonic_buffer_resource that will use the larger of 182 | * an internal initial buffer size and the buffer size specified here 183 | * as the size of the initial buffer requested from the provided 184 | * upstream memory_resource 185 | */ 186 | monotonic_buffer_resource (std::size_t initial_size, 187 | memory_resource* upstream) noexcept; 188 | 189 | /** Calls release() */ 190 | ~monotonic_buffer_resource() override; 191 | 192 | monotonic_buffer_resource& operator= (const monotonic_buffer_resource&) = delete; 193 | monotonic_buffer_resource (const monotonic_buffer_resource&) = delete; 194 | 195 | /** Deallocate all blocks of memory allocated from upstream */ 196 | void release(); 197 | 198 | /** Get this instance's upstream memory_resource */ 199 | memory_resource* upstream_resource() const; 200 | 201 | private: 202 | void* do_allocate(std::size_t bytes, std::size_t align) override; 203 | void do_deallocate(void*, std::size_t bytes, std::size_t align) override; 204 | void recalculate_next_buffer_size(); 205 | 206 | memory_resource& upstream; 207 | void* currentbuf; 208 | std::size_t currentbuf_size; 209 | std::size_t nextbuf_size; 210 | detail::chunk_list chunks; 211 | }; 212 | 213 | //============================================================================== 214 | /** Configuration options for a pool_resource. 215 | * - largest_required_pool_block will clamped to max_u32. 216 | * - max_blocks_per_chunk will be constrained such that no pool 217 | * can exceed max_u32 bytes in size. 218 | */ 219 | struct pool_options 220 | { 221 | std::size_t max_blocks_per_chunk; 222 | std::size_t largest_required_pool_block; 223 | }; 224 | 225 | //============================================================================== 226 | /** A memory_resource backed by a series of memory pools, structured 227 | * by size. This is a very fast way to allocate heterogeneous memory 228 | * regions, but it's not always perfectly economical with memory. 229 | * For one thing, a request of e.g. 17 bytes will be placed into a 230 | * block of 32 bytes in size. A case that results in even greater 231 | * wastage is when a large number of blocks of the same size are 232 | * allocated, then deallocated, and then no more blocks of that 233 | * size are required again. The area that was used for those 234 | * blocks will not be reclaimed until release() is called to reset 235 | * the whole resource. 236 | * 237 | * - This class is not threadsafe. 238 | * - Allocations larger than pool_options.largest_required_pool_block 239 | * are serviced by the upstream resource. 240 | * - Alignment must be a power of two, just like system malloc. 241 | * - Allocations for overaligned (> alignof(std::max_align_t)) memory 242 | * are serviced by the upstream pool. 243 | * - Allocations for any other alignment are always aligned to 244 | * alignof(std::max_align_t). 245 | * - Individual allocations may not exceed 2^32 bytes in size. 246 | */ 247 | class unsynchronized_pool_resource : public identity_equal_resource 248 | { 249 | public: 250 | /** Instantiate using a default-constructed pool_options and 251 | * the memory_resource returned by pmr::get_default_resource() 252 | */ 253 | unsynchronized_pool_resource(); 254 | 255 | /** Instantiate using a default-constructed pool_options and the 256 | * provided memory_resource 257 | */ 258 | explicit unsynchronized_pool_resource (memory_resource* upstream); 259 | 260 | /** Instantiate using the supplied pool_options and memory_resource */ 261 | explicit unsynchronized_pool_resource (const pool_options& opts, 262 | memory_resource* upstream); 263 | 264 | /** Calls release() */ 265 | ~unsynchronized_pool_resource() override; 266 | 267 | unsynchronized_pool_resource& operator= (const unsynchronized_pool_resource&) = delete; 268 | unsynchronized_pool_resource (const unsynchronized_pool_resource&) = delete; 269 | 270 | /** Frees all memory allocated via this object, even if that memory 271 | * has not been deallocated. 272 | */ 273 | void release(); 274 | 275 | /** Access the upstream memory resource used by this instance. */ 276 | memory_resource* upstream_resource() const; 277 | 278 | /** Access the pool_options used by this instance. Note that while the 279 | * pool_options instance passed to the constructor is a hint, the 280 | * value returned from this function reflects the actual pool sizing 281 | * values chosen by the implementation. 282 | */ 283 | pool_options options() const; 284 | 285 | protected: 286 | void* do_allocate (std::size_t bytes, std::size_t align) override; 287 | void do_deallocate (void* ptr, std::size_t bytes, std::size_t align) override; 288 | void init_pools(); 289 | 290 | private: 291 | detail::pool& which_pool (std::size_t bytes); 292 | bool is_oversized (std::size_t bytes, std::size_t align) const; 293 | 294 | pool_options opts; 295 | memory_resource& upstream; 296 | std::vector> pools; 297 | detail::chunk_list oversized; 298 | }; 299 | 300 | 301 | //============================================================================ 302 | // ************************** IMPLEMENTATION ********************************* 303 | //============================================================================ 304 | inline cradle::pmr::aligned_new_delete_resource default_new_delete_resource; 305 | inline cradle::pmr::null_resource default_null_resource; 306 | inline std::atomic default_resource = &default_new_delete_resource; 307 | 308 | inline memory_resource* get_default_resource() noexcept { return default_resource.load(); } 309 | inline memory_resource* set_default_resource (memory_resource* r) noexcept { return default_resource.exchange (r); } 310 | inline memory_resource* null_memory_resource() noexcept { return &default_null_resource; } 311 | inline memory_resource* new_delete_resource() noexcept { return &default_new_delete_resource; } 312 | 313 | //============================================================================== 314 | namespace detail 315 | { 316 | inline std::size_t mono_default_nextbuf_size = 32 * sizeof(void*); 317 | inline std::size_t max_u32 = (std::size_t) std::numeric_limits::max(); 318 | 319 | inline pool_options default_pool_options = {std::size_t (1 << 15), 4096}; 320 | 321 | // Note: Undefined behaviour if 'align' is not a power of two! 322 | constexpr std::size_t aligned_ceil (size_t size, size_t align) noexcept 323 | { 324 | return (size + align - 1) & ~(align - 1); 325 | } 326 | } 327 | 328 | //============================================================================== 329 | inline void* detail::chunk_list::allocate (std::size_t num_bytes, std::size_t align, memory_resource& upstream) 330 | { 331 | if (num_bytes == 0 || num_bytes >= max_u32) 332 | throw std::bad_alloc(); 333 | 334 | num_bytes += header_size(); 335 | align = std::max (align, alignof(chunk)); 336 | 337 | auto* ptr = upstream.allocate (num_bytes, align); 338 | 339 | if (auto* next = std::exchange (first, ::new (ptr) chunk())) 340 | { 341 | first->next = next; 342 | next->prev = first; 343 | } 344 | 345 | first->size = std::uint32_t (num_bytes); 346 | first->align = std::uint32_t (align); 347 | return (std::byte*) ptr + header_size(); 348 | } 349 | 350 | inline void detail::chunk_list::release (memory_resource& upstream) 351 | { 352 | while (first != nullptr) 353 | { 354 | auto next = first->next; 355 | upstream.deallocate (first, first->size, first->align); 356 | first = next; 357 | } 358 | } 359 | 360 | inline void detail::chunk_list::deallocate_chunk (void* address, memory_resource& upstream) 361 | { 362 | auto* c = reinterpret_cast ((std::byte*) address - header_size()); 363 | 364 | if (c->prev != nullptr) 365 | c->prev->next = c->next; 366 | if (c->next != nullptr) 367 | c->next->prev = c->prev; 368 | if (c == first) 369 | first = c->next; 370 | 371 | upstream.deallocate (c, c->size, c->align); 372 | } 373 | 374 | inline constexpr std::size_t detail::chunk_list::header_size() 375 | { 376 | constexpr auto size = aligned_ceil (sizeof(chunk), max_alignment); 377 | static_assert(size <= sizeof(std::size_t) * 4, "Let's keep the chunk_list header small!"); 378 | return size; 379 | } 380 | 381 | //============================================================================== 382 | inline monotonic_buffer_resource::monotonic_buffer_resource() noexcept 383 | : monotonic_buffer_resource (detail::mono_default_nextbuf_size, nullptr) 384 | {} 385 | 386 | inline monotonic_buffer_resource::monotonic_buffer_resource (memory_resource* up) noexcept 387 | : monotonic_buffer_resource (detail::mono_default_nextbuf_size, up) 388 | {} 389 | 390 | inline monotonic_buffer_resource::monotonic_buffer_resource (std::size_t initial_size) noexcept 391 | : monotonic_buffer_resource (initial_size, nullptr) 392 | {} 393 | 394 | inline monotonic_buffer_resource::monotonic_buffer_resource (std::size_t initial_size, 395 | memory_resource* up) noexcept 396 | : upstream (up ? *up : *get_default_resource()), 397 | currentbuf (nullptr), 398 | currentbuf_size (0), 399 | nextbuf_size (std::max (initial_size, detail::mono_default_nextbuf_size)) 400 | {} 401 | 402 | inline monotonic_buffer_resource::monotonic_buffer_resource (void* buf, std::size_t bufsize) noexcept 403 | : monotonic_buffer_resource (buf, bufsize, nullptr) 404 | {} 405 | 406 | 407 | inline monotonic_buffer_resource::monotonic_buffer_resource (void* buf, 408 | std::size_t bufsize, 409 | memory_resource* up) noexcept 410 | : upstream (up ? *up : *get_default_resource()), 411 | currentbuf (buf), 412 | currentbuf_size (bufsize), 413 | nextbuf_size (std::max (bufsize, detail::mono_default_nextbuf_size)) 414 | { 415 | recalculate_next_buffer_size(); 416 | } 417 | 418 | inline monotonic_buffer_resource::~monotonic_buffer_resource() 419 | { 420 | release(); 421 | } 422 | 423 | 424 | inline void monotonic_buffer_resource::release() 425 | { 426 | chunks.release (upstream); 427 | } 428 | 429 | 430 | inline memory_resource* monotonic_buffer_resource::upstream_resource() const 431 | { 432 | return &upstream; 433 | } 434 | 435 | 436 | inline void* monotonic_buffer_resource::do_allocate (std::size_t bytes, std::size_t align) 437 | { 438 | if (bytes == 0) 439 | throw std::bad_alloc(); 440 | 441 | void* allocated = std::align (align, bytes, currentbuf, currentbuf_size); 442 | 443 | if (allocated == nullptr) 444 | { 445 | nextbuf_size = std::max (nextbuf_size, detail::aligned_ceil (bytes, align)); 446 | currentbuf = chunks.allocate (nextbuf_size, align, upstream); 447 | currentbuf_size = nextbuf_size; 448 | recalculate_next_buffer_size(); 449 | allocated = std::align (align, bytes, currentbuf, currentbuf_size); 450 | 451 | if (allocated == nullptr) 452 | throw std::bad_alloc(); 453 | } 454 | 455 | currentbuf = reinterpret_cast (currentbuf) + bytes; 456 | currentbuf_size -= bytes; 457 | return allocated; 458 | } 459 | 460 | 461 | inline void monotonic_buffer_resource::recalculate_next_buffer_size() 462 | { 463 | nextbuf_size = (detail::max_u32/2 < nextbuf_size) ? detail::max_u32 : currentbuf_size * 2; 464 | } 465 | 466 | 467 | inline void monotonic_buffer_resource::do_deallocate (void*, std::size_t, std::size_t) 468 | { 469 | // Do nothing: this resource doesn't deallocate. 470 | } 471 | 472 | //============================================================================== 473 | inline detail::pool::pool (memory_resource& r, block_size_t block_size_in_bytes) 474 | : chunks (&r), 475 | block_size (block_size_in_bytes) 476 | { 477 | // invalid block_size given to memory pool 478 | assert (block_size >= min_block_size); 479 | } 480 | 481 | inline void* detail::pool::allocate (const pool_options& opts) 482 | { 483 | if (next_free != nullptr) 484 | return std::exchange (next_free, next_free->next); 485 | 486 | auto& chunk = find_or_alloc_chunk_with_space (opts); 487 | return chunk.address (chunk.num_initialized++, block_size); 488 | } 489 | 490 | inline void detail::pool::deallocate (void* block) 491 | { 492 | next_free = ::new (block) free_link (next_free); 493 | } 494 | 495 | inline auto detail::pool::find_or_alloc_chunk_with_space (const pool_options& opts) -> chunk& 496 | { 497 | if (! chunks.empty()) 498 | if (auto& last = chunks.back(); last.num_initialized < last.num_blocks) 499 | return last; 500 | 501 | if (chunks.capacity() == 0) 502 | chunks.reserve (8); 503 | 504 | const auto max_num_blocks = block_size_t (opts.max_blocks_per_chunk); 505 | const auto min_num_blocks = std::clamp (block_size_t (opts.largest_required_pool_block) / (block_size/2), 2u, 512u); 506 | 507 | auto num_blocks = std::min (max_num_blocks, chunks.empty() ? min_num_blocks : chunks.back().num_blocks * 2); 508 | auto& resource = *chunks.get_allocator().resource(); 509 | auto* buffer = (std::byte*) resource.allocate (block_size * num_blocks, max_alignment); 510 | return chunks.emplace_back (num_blocks, buffer); 511 | } 512 | 513 | inline void detail::pool::release() 514 | { 515 | auto& resource = *chunks.get_allocator().resource(); 516 | 517 | for (auto& chunk : chunks) 518 | resource.deallocate (chunk.block_buffer, chunk.num_blocks * block_size, max_alignment); 519 | 520 | chunks.clear(); 521 | } 522 | 523 | inline std::byte* detail::pool::chunk::address (block_size_t index, block_size_t block_size_) const 524 | { 525 | return block_buffer + (index * block_size_); 526 | } 527 | 528 | //============================================================================== 529 | inline unsynchronized_pool_resource::unsynchronized_pool_resource() 530 | : unsynchronized_pool_resource (nullptr) 531 | {} 532 | 533 | inline unsynchronized_pool_resource::unsynchronized_pool_resource (memory_resource* resource) 534 | : unsynchronized_pool_resource (detail::default_pool_options, resource) 535 | {} 536 | 537 | inline unsynchronized_pool_resource::unsynchronized_pool_resource (const pool_options& opts_, 538 | memory_resource* resource) 539 | 540 | : opts (opts_), 541 | upstream (resource ? *resource : *get_default_resource()), 542 | pools (&upstream) 543 | { 544 | opts.largest_required_pool_block = std::min (opts.largest_required_pool_block, detail::max_u32); 545 | opts.max_blocks_per_chunk = std::min (opts.max_blocks_per_chunk, detail::max_u32 / opts.largest_required_pool_block); 546 | } 547 | 548 | inline unsynchronized_pool_resource::~unsynchronized_pool_resource() 549 | { 550 | release(); 551 | } 552 | 553 | inline void unsynchronized_pool_resource::release() 554 | { 555 | for (auto& pool : pools) 556 | pool.release(); 557 | 558 | oversized.release (upstream); 559 | } 560 | 561 | inline memory_resource* unsynchronized_pool_resource::upstream_resource() const 562 | { 563 | return &upstream; 564 | } 565 | 566 | inline pool_options unsynchronized_pool_resource::options() const 567 | { 568 | return opts; 569 | } 570 | 571 | inline void unsynchronized_pool_resource::init_pools() 572 | { 573 | using block_size_t = detail::pool::block_size_t; 574 | assert (opts.largest_required_pool_block <= detail::max_u32); 575 | assert (opts.max_blocks_per_chunk <= detail::max_u32); 576 | 577 | const auto largest_block = (block_size_t) opts.largest_required_pool_block; 578 | auto num_pools = block_size_t (0); 579 | 580 | for (block_size_t n = detail::pool::min_block_size; n <= largest_block; n *= 2) 581 | ++num_pools; 582 | 583 | pools.reserve (num_pools); // avoid reallocations while building vector 584 | 585 | for (block_size_t n = detail::pool::min_block_size; n <= largest_block; n *= 2) 586 | pools.emplace_back (upstream, n); 587 | } 588 | 589 | inline detail::pool& unsynchronized_pool_resource::which_pool (std::size_t bytes) 590 | { 591 | for (auto& pool : pools) 592 | if (pool.get_block_size() >= bytes) 593 | return pool; 594 | 595 | throw std::logic_error ("unsynchronized_pool_resource can't find appropriate pool"); 596 | } 597 | 598 | inline void* unsynchronized_pool_resource::do_allocate (std::size_t bytes, std::size_t align) 599 | { 600 | if (bytes == 0 || bytes > detail::max_u32) 601 | throw std::bad_alloc(); 602 | 603 | if (is_oversized (bytes, align)) 604 | return oversized.allocate (bytes, align, upstream); 605 | 606 | // If we don't do this lazily, testing that pool_options is constrained 607 | // becomes hard, because the constructor will allocate vast regions of 608 | // memory when we use big values. Keep an eye on this branch's cost though. 609 | if (pools.empty()) 610 | init_pools(); 611 | 612 | return which_pool (bytes).allocate (opts); 613 | } 614 | 615 | inline void unsynchronized_pool_resource::do_deallocate (void* ptr, std::size_t bytes, std::size_t align) 616 | { 617 | if (is_oversized (bytes, align)) 618 | oversized.deallocate_chunk (ptr, upstream); 619 | else 620 | which_pool (bytes).deallocate (ptr); 621 | } 622 | 623 | inline bool unsynchronized_pool_resource::is_oversized (std::size_t bytes, std::size_t align) const 624 | { 625 | return bytes > opts.largest_required_pool_block || align > alignof(std::max_align_t); 626 | } 627 | 628 | } // namespace cradle::pmr 629 | 630 | PMR_DIAGNOSTIC_POP 631 | -------------------------------------------------------------------------------- /tests/resource_tests.cpp: -------------------------------------------------------------------------------- 1 | //============================================================================== 2 | // Copyright (c) 2019-2023 CradleApps, LLC - All Rights Reserved 3 | //============================================================================== 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "realtime_memory/memory_resources.h" 11 | #include "realtime_memory/free_list_resource.h" 12 | #include "realtime_memory/containers.h" 13 | 14 | PMR_DIAGNOSTIC_PUSH 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | //============================================================================== 23 | 24 | /** To track the allocations farmed out to a memory_resource's "upstream". */ 25 | class tracking_memory_resource : public std_pmr::memory_resource 26 | { 27 | public: 28 | void* do_allocate (std::size_t bytes, std::size_t align) override 29 | { 30 | // Since malloc gives use max aligned pointers, we don't need to pad 31 | // for alignments less than that to allow space for alignment. 32 | auto size = align > alignof(std::max_align_t) ? bytes + align : bytes; 33 | auto ptr = std::malloc (size); 34 | auto ptr_front = ptr; 35 | auto ptr_aligned = std::align (align, bytes, ptr /*mutated*/, size); 36 | records.push_back (record {ptr_front, ptr_aligned, size}); 37 | return ptr_aligned; 38 | } 39 | 40 | void do_deallocate (void* ptr, std::size_t /*bytes*/, std::size_t /*align*/) override 41 | { 42 | auto match = [ptr] (record& r) { return r.ptr_aligned == ptr; }; 43 | 44 | if (auto it = std::find_if (records.begin(), records.end(), match); it != records.end()) 45 | { 46 | std::free (it->ptr); 47 | records.erase (it); 48 | return; 49 | } 50 | 51 | throw std::logic_error ("Logic error in the test fixture!"); 52 | } 53 | 54 | bool do_is_equal (const memory_resource&) const noexcept override 55 | { 56 | return false; 57 | } 58 | 59 | std::size_t total_allocated() const 60 | { 61 | return std::accumulate (records.begin(), records.end(), std::size_t (0), 62 | [] (std::size_t n, const record& r) { return n + r.num_bytes; }); 63 | } 64 | 65 | std::size_t allocations_count() const 66 | { 67 | return records.size(); 68 | } 69 | 70 | private: 71 | struct record 72 | { 73 | void* ptr; 74 | void* ptr_aligned; 75 | std::size_t num_bytes; 76 | }; 77 | 78 | std::vector records; 79 | }; 80 | 81 | //============================================================================== 82 | /** A few helpers shared between various tests. */ 83 | namespace helpers 84 | { 85 | bool isInsideBuffer (void* ptrToCheck, const std::vector& buffer) 86 | { 87 | return ptrToCheck >= buffer.data() && ptrToCheck < (buffer.data() + buffer.size()); 88 | } 89 | 90 | cradle::pmr::string generateRandomString (Catch::Generators::GeneratorWrapper& rnd, cradle::pmr::memory_resource& r) 91 | { 92 | cradle::pmr::string s (&r); 93 | s.reserve (rnd.get()); 94 | 95 | for (std::size_t i = 0; i < rnd.get(); ++i) 96 | s.push_back ('?'); 97 | 98 | rnd.next(); 99 | return s; 100 | } 101 | 102 | constexpr std::size_t numTestAllocs = 1 << 13; 103 | } 104 | 105 | //============================================================================== 106 | TEST_CASE ("get/set default resource") 107 | { 108 | namespace pmr = cradle::pmr; 109 | 110 | SECTION ("Initial default is new_delete") 111 | { 112 | CHECK (pmr::get_default_resource() == pmr::new_delete_resource()); 113 | } 114 | 115 | SECTION ("Can set a different default resource") 116 | { 117 | CHECK (pmr::set_default_resource (pmr::null_memory_resource())); 118 | CHECK (pmr::get_default_resource() == pmr::null_memory_resource()); 119 | CHECK (pmr::set_default_resource (pmr::new_delete_resource())); // restore default 120 | } 121 | } 122 | 123 | //============================================================================== 124 | /** Defined in but tested here alongside our custom 'recycling' resource. */ 125 | TEST_CASE ("monotonic_buffer_resource", "[memory_resource]") 126 | { 127 | namespace pmr = cradle::pmr; 128 | 129 | SECTION ("Only compares equal with itself") 130 | { 131 | pmr::monotonic_buffer_resource mono1; 132 | pmr::monotonic_buffer_resource mono2; 133 | 134 | CHECK (mono1 == mono1); 135 | CHECK_FALSE (mono1 == mono2); 136 | } 137 | 138 | SECTION ("Non-copyable") 139 | { 140 | static_assert (! std::is_copy_constructible_v, 141 | "monotonic_buffer_resource must not be copy constructible"); 142 | static_assert (! std::is_copy_assignable_v, 143 | "monotonic_buffer_resource must not be copy assignable"); 144 | } 145 | 146 | SECTION ("Uses default memory_resource as upstream") 147 | { 148 | tracking_memory_resource res; 149 | pmr::set_default_resource (&res); 150 | 151 | pmr::unsynchronized_pool_resource pool; 152 | CHECK (pool.upstream_resource() == &res); 153 | 154 | pmr::set_default_resource (pmr::new_delete_resource()); 155 | } 156 | 157 | SECTION ("Uses supplied memory_resource as upstream") 158 | { 159 | pmr::monotonic_buffer_resource mono (pmr::null_memory_resource()); 160 | 161 | CHECK (mono.upstream_resource() == pmr::null_memory_resource()); 162 | } 163 | 164 | SECTION ("Uses initial_size hint") 165 | { 166 | tracking_memory_resource upstream; 167 | pmr::monotonic_buffer_resource mono (3000, &upstream); 168 | auto p = mono.allocate (1); 169 | 170 | // It may be bigger, as it's allowed to allocate for internal bookkeeping. 171 | CHECK (upstream.total_allocated() >= 3000); 172 | 173 | mono.deallocate (p, 1); // don't leak, using malloc as a source. 174 | } 175 | 176 | SECTION ("Uses supplied buffer until full") 177 | { 178 | std::vector buffer (2048, std::byte(0)); 179 | pmr::monotonic_buffer_resource mono (buffer.data(), buffer.size()); 180 | 181 | CHECK (mono.upstream_resource() == pmr::get_default_resource()); 182 | 183 | auto ptr1 = mono.allocate (1024, 1); 184 | auto ptr2 = mono.allocate (1024, 1); 185 | 186 | CHECK (helpers::isInsideBuffer (ptr1, buffer)); 187 | CHECK (helpers::isInsideBuffer (ptr2, buffer)); 188 | 189 | auto ptr3 = mono.allocate (16, 1); 190 | CHECK_FALSE (helpers::isInsideBuffer (ptr3, buffer)); 191 | } 192 | 193 | SECTION ("Uses supplied buffer, falling back to supplied memory_resource") 194 | { 195 | std::vector buffer (2048, std::byte(0)); 196 | pmr::monotonic_buffer_resource mono (buffer.data(), buffer.size(), pmr::null_memory_resource()); 197 | 198 | auto ptr1 = mono.allocate (1024, 1); 199 | auto ptr2 = mono.allocate (1024, 1); 200 | 201 | CHECK (helpers::isInsideBuffer (ptr1, buffer)); 202 | CHECK (helpers::isInsideBuffer (ptr2, buffer)); 203 | CHECK_THROWS_AS (mono.allocate (16), std::bad_alloc); 204 | } 205 | 206 | SECTION ("Deallocate is a no-op") 207 | { 208 | std::vector buffer (2048, std::byte(0)); 209 | pmr::monotonic_buffer_resource mono (buffer.data(), buffer.size()); 210 | 211 | auto ptr1 = mono.allocate (1024, 1); 212 | CHECK (ptr1 == buffer.data()); 213 | 214 | mono.deallocate (ptr1, 1024); 215 | 216 | auto ptr2 = mono.allocate (1024, 1); 217 | CHECK (ptr2 == buffer.data() + 1024); 218 | } 219 | 220 | SECTION ("Delivers any power of two alignment") 221 | { 222 | std::vector buffer (2048, std::byte(0)); 223 | pmr::monotonic_buffer_resource mono (buffer.data(), buffer.size()); 224 | 225 | auto ptr1 = mono.allocate (3, 1); 226 | CHECK (ptr1 == buffer.data()); 227 | 228 | auto align = GENERATE (as(), 1, 2, 4, 8, 16, 32, 64); 229 | auto ptr2 = mono.allocate (4, align); 230 | 231 | CHECK (std::size_t (ptr2) % align == 0); 232 | } 233 | } 234 | 235 | //============================================================================== 236 | /** Defined in but tested here alongside our custom 'recycling' resource. */ 237 | TEST_CASE ("unsynchronized_pool_resource", "[memory_resource]") 238 | { 239 | namespace pmr = cradle::pmr; 240 | 241 | SECTION ("Only compares equal with itself") 242 | { 243 | pmr::unsynchronized_pool_resource pool1; 244 | pmr::unsynchronized_pool_resource pool2; 245 | 246 | CHECK (pool1 == pool1); 247 | CHECK_FALSE (pool1 == pool2); 248 | } 249 | 250 | SECTION ("Non-copyable") 251 | { 252 | static_assert (! std::is_copy_constructible_v, 253 | "unsynchronized_pool_resource must not be copy constructible"); 254 | static_assert (! std::is_copy_assignable_v, 255 | "unsynchronized_pool_resource must not be copy assignable"); 256 | } 257 | 258 | SECTION ("Limits pool_options values") 259 | { 260 | constexpr auto max_u64 = std::numeric_limits::max(); 261 | constexpr auto max_u32 = (std::size_t) std::numeric_limits::max(); 262 | const auto maxBlocks = GENERATE (as(), 32ul, 1 << 15, max_u64 - 1); 263 | const auto largestBlock = GENERATE (as(), 32ul, 1 << 15, max_u64 - 1); 264 | CAPTURE (maxBlocks, largestBlock); 265 | 266 | pmr::pool_options options {maxBlocks, largestBlock}; 267 | pmr::unsynchronized_pool_resource pool (options, nullptr); 268 | 269 | const auto expectedLargestBlock = std::min (largestBlock, max_u32); 270 | const auto expectedMaxBlocks = std::min (maxBlocks, max_u32 / expectedLargestBlock); 271 | CHECK (pool.options().largest_required_pool_block == expectedLargestBlock); 272 | CHECK (pool.options().max_blocks_per_chunk == expectedMaxBlocks); 273 | } 274 | 275 | SECTION ("Uses default memory_resource as upstream") 276 | { 277 | tracking_memory_resource res; 278 | pmr::set_default_resource (&res); 279 | 280 | pmr::unsynchronized_pool_resource pool; 281 | CHECK (pool.upstream_resource() == &res); 282 | 283 | pmr::set_default_resource (pmr::new_delete_resource()); 284 | } 285 | 286 | SECTION ("Uses supplied memory_resource as upstream") 287 | { 288 | pmr::monotonic_buffer_resource upstream; 289 | pmr::unsynchronized_pool_resource pool (&upstream); 290 | 291 | CHECK (pool.upstream_resource() == &upstream); 292 | } 293 | 294 | SECTION ("Oversized allocations are serviced by the upstream resource") 295 | { 296 | tracking_memory_resource src; 297 | pmr::unsynchronized_pool_resource pool (&src); 298 | 299 | auto bytes = pool.options().largest_required_pool_block + 1; 300 | auto ptr1 = pool.allocate (bytes, 8); 301 | auto numFromUpstream = src.allocations_count(); 302 | auto ptr2 = pool.allocate (bytes, 8); 303 | 304 | CHECK (src.allocations_count() > numFromUpstream); 305 | 306 | pool.deallocate (ptr1, bytes, 8); 307 | pool.deallocate (ptr2, bytes, 8); 308 | } 309 | 310 | SECTION ("Overaligned allocations are serviced by the upstream resource") 311 | { 312 | tracking_memory_resource src; 313 | pmr::unsynchronized_pool_resource pool (&src); 314 | 315 | auto align = alignof(std::max_align_t) * GENERATE (as(), 2, 4, 8); 316 | auto ptr1 = pool.allocate (3, align); 317 | auto numFromUpstream = src.allocations_count(); 318 | auto ptr2 = pool.allocate (3, align); 319 | 320 | CHECK (src.allocations_count() > numFromUpstream); 321 | 322 | pool.deallocate (ptr1, 3, align); 323 | pool.deallocate (ptr2, 3, align); 324 | } 325 | 326 | SECTION ("Delivers alignment up to alignof(std::max_align_t)") 327 | { 328 | tracking_memory_resource src; 329 | pmr::unsynchronized_pool_resource pool (&src); 330 | 331 | auto max = pool.options().largest_required_pool_block + 7; 332 | auto bytes = GENERATE_COPY (as(), 1, 3, 7, 8, 133, max); 333 | auto align = GENERATE (as(), 1, 2, 4, 8, alignof(std::max_align_t)); 334 | CAPTURE (bytes, align); 335 | 336 | auto ptr1 = pool.allocate (3, 1); 337 | auto ptr2 = pool.allocate (bytes, align); 338 | 339 | CHECK (std::size_t (ptr2) % align == 0); 340 | 341 | pool.deallocate (ptr1, 3, 1); 342 | pool.deallocate (ptr2, bytes, align); 343 | } 344 | } 345 | 346 | //============================================================================== 347 | /** Our custom 'recycling' resource. */ 348 | TEST_CASE ("free_list_resource basics", "[memory_resource]") 349 | { 350 | namespace pmr = cradle::pmr; 351 | 352 | std::vector buf (256, std::byte (0)); 353 | pmr::free_list_resource res (buf.data(), buf.size()); 354 | 355 | SECTION ("Only compares equal with itself") 356 | { 357 | pmr::free_list_resource res2 (*pmr::get_default_resource(), 256); 358 | pmr::free_list_resource res3 (*pmr::get_default_resource(), 256); 359 | 360 | CHECK (res == res); 361 | CHECK (res2 == res2); 362 | CHECK_FALSE (res == res2); 363 | CHECK_FALSE (res2 == res3); 364 | } 365 | 366 | SECTION ("Non-copyable") 367 | { 368 | static_assert (! std::is_copy_constructible_v, 369 | "FreeListBufferResource must not be copy constructible"); 370 | static_assert (! std::is_copy_assignable_v, 371 | "FreeListBufferResource must not be copy assignable"); 372 | } 373 | 374 | SECTION ("Throws bad_alloc on a request for zero memory") 375 | { 376 | CHECK_THROWS_AS (res.allocate (0, 1), std::bad_alloc); 377 | } 378 | 379 | SECTION ("Throws bad_alloc on a request for overaligned memory") 380 | { 381 | auto align = alignof(std::max_align_t) * 2; 382 | 383 | CHECK_THROWS_AS (res.allocate (4, align), std::bad_alloc); 384 | } 385 | 386 | SECTION ("Delivers alignment up to alignof(std::max_align_t)") 387 | { 388 | auto align = GENERATE (as(), 1, 2, 4, alignof(std::max_align_t)); 389 | auto bytes = GENERATE (as(), 1, 3, 7, 8, 41, 77); 390 | CAPTURE (bytes, align); 391 | 392 | auto ptr1 = res.allocate (3, 1); 393 | auto ptr2 = res.allocate (bytes, align); 394 | 395 | CHECK (std::size_t (ptr2) % align == 0); 396 | 397 | res.deallocate (ptr1, 3, 1); 398 | res.deallocate (ptr2, bytes, align); 399 | } 400 | } 401 | 402 | TEST_CASE ("free_list_resource (backed by buffer)", "[memory_resource]") 403 | { 404 | namespace pmr = cradle::pmr; 405 | std::vector buf1 (256, std::byte(0)), buf2 (256, std::byte(0)); 406 | 407 | SECTION ("Throws bad_alloc when the buffer is exhausted") 408 | { 409 | pmr::free_list_resource res (buf1.data(), buf1.size()); 410 | 411 | CHECK_NOTHROW (res.allocate (buf1.size() - 32, 4)); 412 | CHECK_THROWS_AS (res.allocate (32, 4), std::bad_alloc); 413 | } 414 | 415 | SECTION ("Re-merges split blocks that have been returned (defragmentation)") 416 | { 417 | std::vector buf (512, std::byte(0)); 418 | std::vector ptrs; 419 | 420 | pmr::free_list_resource res (buf.data(), buf.size()); 421 | 422 | const std::size_t alignment = 1; 423 | const std::size_t smallBlockSize = 2; 424 | const std::size_t largeBlockSize = 200; 425 | 426 | // Allocate small blocks that split up the free list. 427 | // We don't know the exact capacity of the resource due 428 | // to its internal bookkeeping, so we keep going until it throws. 429 | try 430 | { 431 | while (true) 432 | ptrs.push_back (res.allocate (smallBlockSize, alignment)); 433 | } 434 | catch (const std::bad_alloc&) 435 | {} 436 | 437 | // Return all the blocks except one in the middle. 438 | // We do this in random order, to ensure the free list must be sorted. 439 | const auto middlePtr = ptrs[ptrs.size() / 2]; 440 | const auto seed = Catch::Generators::Detail::getSeed(); 441 | std::shuffle (ptrs.begin(), ptrs.end(), std::default_random_engine (seed)); 442 | 443 | for (auto p : ptrs) 444 | if (p != middlePtr) 445 | res.deallocate (p, smallBlockSize, alignment); 446 | 447 | // Check that the two areas either side of the still-active pointer 448 | // can be re-merged in a defragmentation step. 449 | CHECK (res.allocate (largeBlockSize, alignment) != nullptr); 450 | CHECK (res.allocate (largeBlockSize, alignment) != nullptr); 451 | CHECK_THROWS_AS (res.allocate (largeBlockSize, alignment), std::bad_alloc); 452 | } 453 | } 454 | 455 | TEST_CASE ("free_list_resource (backed by upstream resource)", "[memory_resource]") 456 | { 457 | namespace pmr = cradle::pmr; 458 | tracking_memory_resource upstream; 459 | 460 | SECTION ("Requests more memory from upstream when exhausted") 461 | { 462 | constexpr std::size_t initialSize = 256, minChunkSize = 512; 463 | 464 | pmr::free_list_resource res (upstream, initialSize, minChunkSize); 465 | const auto initial = upstream.total_allocated(); 466 | 467 | CHECK (initial == initialSize); 468 | 469 | constexpr auto alloc1Size = initialSize - 32; 470 | constexpr auto alloc2Size = 39; 471 | 472 | auto ptr1 = res.allocate (alloc1Size, 1); 473 | CHECK (upstream.total_allocated() == initial); 474 | 475 | [[maybe_unused]] auto unused1 = res.allocate (alloc2Size, 2); 476 | CHECK (upstream.total_allocated() == initial + minChunkSize); 477 | 478 | SECTION ("Freed memory from both chunks is reusable with no additional allocations") 479 | { 480 | res.deallocate (ptr1, alloc1Size, 1); 481 | 482 | [[maybe_unused]] auto unused2 = res.allocate (alloc1Size, 1); // reuse first chunk 483 | [[maybe_unused]] auto unused3 = res.allocate (minChunkSize - alloc2Size - 64, 1); // use remainder of second chunk (minus header usage). 484 | CHECK (upstream.total_allocated() == initial + minChunkSize); 485 | } 486 | } 487 | 488 | SECTION ("Extra memory chunks can be supplied from outside") 489 | { 490 | pmr::free_list_resource res (upstream, 256); 491 | 492 | [[maybe_unused]] auto unused1 = res.allocate (230, 1); 493 | 494 | res.expand (upstream.allocate (256, 1), 256); 495 | const auto used = upstream.total_allocated(); 496 | 497 | CHECK (res.allocate (160, 1) != nullptr); 498 | CHECK (upstream.total_allocated() == used); 499 | } 500 | 501 | SECTION ("Additional chunks supplied from outside can all be used") 502 | { 503 | constexpr std::size_t minChunkSize = 512; 504 | pmr::free_list_resource res (upstream, 256, minChunkSize); 505 | 506 | res.expand (upstream.allocate (256, 1), 256); 507 | res.expand (upstream.allocate (256, 1), 256); 508 | res.expand (upstream.allocate (256, 1), 256); 509 | 510 | const auto used = upstream.total_allocated(); 511 | 512 | auto ptr1 = res.allocate (200, 1); 513 | auto ptr2 = res.allocate (200, 1); 514 | auto ptr3 = res.allocate (200, 1); 515 | auto ptr4 = res.allocate (200, 1); 516 | CHECK_FALSE (ptr1 == nullptr); 517 | CHECK_FALSE (ptr2 == nullptr); 518 | CHECK_FALSE (ptr3 == nullptr); 519 | CHECK_FALSE (ptr4 == nullptr); 520 | 521 | CHECK (upstream.total_allocated() == used); 522 | 523 | SECTION ("Allocation that cannot be satisfied (allocate from upstream)") 524 | { 525 | // Now it must allocate, because no single chunk has the required space left 526 | CHECK (res.allocate (200, 1) != nullptr); 527 | CHECK (upstream.total_allocated() == used + minChunkSize); 528 | } 529 | 530 | SECTION ("Free then allocate into one of the existing chunks") 531 | { 532 | // check smaller, equal and larger sizes (checks de-frag). 533 | auto size = GENERATE (as(), 160, 200, 230); 534 | 535 | res.deallocate (ptr2, 200, 1); 536 | CHECK (res.allocate (size, 1) != nullptr); 537 | CHECK (upstream.total_allocated() == used); 538 | } 539 | } 540 | 541 | SECTION ("Additional chunks that are sequential can be de-fragmented into one") 542 | { 543 | pmr::free_list_resource res (upstream, 256); 544 | REQUIRE (upstream.total_allocated() == 256); 545 | 546 | std::vector buf (768, std::byte (0)); 547 | res.expand (buf.data(), 256); 548 | res.expand (buf.data() + 256, 256); 549 | res.expand (buf.data() + 512, 256); 550 | 551 | auto ptr1 = res.allocate (200, 1); 552 | auto ptr2 = res.allocate (200, 1); 553 | auto ptr3 = res.allocate (200, 1); 554 | auto ptr4 = res.allocate (200, 1); 555 | REQUIRE_FALSE (ptr1 == nullptr); 556 | REQUIRE_FALSE (ptr2 == nullptr); 557 | REQUIRE_FALSE (ptr3 == nullptr); 558 | REQUIRE_FALSE (ptr4 == nullptr); 559 | REQUIRE (upstream.total_allocated() == 256); 560 | 561 | res.deallocate (ptr1, 200); 562 | res.deallocate (ptr2, 200); 563 | res.deallocate (ptr3, 200); 564 | res.deallocate (ptr4, 200); 565 | 566 | SECTION ("When defragmentation can find a large enough block") 567 | { 568 | CHECK (res.allocate (710, 1) != nullptr); // triggers defragmentation 569 | CHECK (upstream.total_allocated() == 256); // served from existing chunks 570 | } 571 | 572 | SECTION ("When defragmentation can't find a block") 573 | { 574 | CHECK (res.allocate (780, 1) != nullptr); // too large, even after degfragmentation 575 | auto used = upstream.total_allocated(); 576 | CHECK (used > 256); // served from upstream 577 | 578 | CHECK (res.allocate (720, 1) != nullptr); // can still use block found during defragmentation 579 | CHECK (upstream.total_allocated() == used); 580 | } 581 | } 582 | } 583 | 584 | //============================================================================== 585 | /** Common tests that all three resources should satisfy. 586 | * Some of these are just sanity checks that "it doesn't crash". 587 | */ 588 | struct BackingBuffer 589 | { 590 | static constexpr std::size_t size = helpers::numTestAllocs * 1024; 591 | std::vector mem {size, std::byte()}; 592 | }; 593 | 594 | struct MonotonicBufferFixture 595 | { 596 | BackingBuffer buffer; 597 | cradle::pmr::monotonic_buffer_resource resource {buffer.mem.data(), buffer.size}; 598 | }; 599 | 600 | struct FreeListFixture 601 | { 602 | BackingBuffer buffer; 603 | cradle::pmr::free_list_resource resource {buffer.mem.data(), buffer.size}; 604 | 605 | std::size_t allocationsCount() const { return resource.num_allocations(); } 606 | std::size_t allocationsSize() const { return buffer.size - resource.available_space(); } 607 | }; 608 | 609 | struct UnsyncPoolFixture 610 | { 611 | FreeListFixture src; 612 | cradle::pmr::unsynchronized_pool_resource resource {&src.resource}; 613 | 614 | std::size_t allocationsCount() const { return src.allocationsCount(); } 615 | std::size_t allocationsSize() const { return src.allocationsSize(); } 616 | }; 617 | 618 | TEMPLATE_TEST_CASE ("memory_resource common tests", "[memory_resource]", 619 | MonotonicBufferFixture, UnsyncPoolFixture, FreeListFixture) 620 | { 621 | namespace pmr = cradle::pmr; 622 | 623 | TestType fixture; 624 | auto& resource = fixture.resource; 625 | 626 | constexpr std::size_t numAllocs = helpers::numTestAllocs; 627 | 628 | SECTION ("Zero sized allocation") 629 | { 630 | CHECK_THROWS (resource.allocate (0, 4)); 631 | } 632 | 633 | SECTION ("Single sized allocations") 634 | { 635 | std::unordered_set ptrs; 636 | 637 | constexpr auto alignment = 4; 638 | const auto numBytes = GENERATE (as(), 8, 11, 16, 19, 32, 33, 170, 256); 639 | 640 | for (std::size_t i = 0; i < numAllocs; ++i) 641 | ptrs.insert (resource.allocate (numBytes, alignment)); 642 | 643 | SECTION ("Yields unique block addresses") 644 | { 645 | CHECK (ptrs.size() == numAllocs); 646 | } 647 | 648 | SECTION ("Can free blocks at random") 649 | { 650 | for (auto it = ptrs.begin(); ptrs.size() > numAllocs / 2; it = ptrs.erase (it)) 651 | resource.deallocate (*it, numBytes, alignment); 652 | 653 | CHECK_NOTHROW (ptrs.insert (resource.allocate (numBytes, alignment))); 654 | } 655 | 656 | for (auto& ptr : ptrs) 657 | resource.deallocate (ptr, numBytes, alignment); 658 | } 659 | 660 | SECTION ("Mixed size allocations") 661 | { 662 | auto rnd = Catch::Generators::random (1, 512); 663 | 664 | std::unordered_map ptrs; 665 | constexpr auto alignment = 4; 666 | 667 | for (std::size_t i = 0; i < numAllocs; ++i, rnd.next()) 668 | { 669 | const auto size = std::size_t (rnd.get()); 670 | const auto ptr = resource.allocate (size, alignment); 671 | ptrs.insert ({ptr, size}); 672 | } 673 | 674 | SECTION ("Yields unique block addresses") 675 | { 676 | CHECK (ptrs.size() == numAllocs); 677 | } 678 | 679 | SECTION ("Can free blocks at random") 680 | { 681 | for (auto it = ptrs.begin(); ptrs.size() > numAllocs / 2; it = ptrs.erase (it)) 682 | resource.deallocate (it->first, it->second); 683 | 684 | CHECK_NOTHROW (resource.allocate (16)); 685 | } 686 | 687 | for (auto& pair : ptrs) 688 | resource.deallocate (pair.first, pair.second, alignment); 689 | } 690 | 691 | SECTION ("Nested container allocations") 692 | { 693 | pmr::vector strings (&resource); 694 | auto rndStrSize = Catch::Generators::random (sizeof(pmr::string), 50); 695 | 696 | for (std::size_t i = 0; i < 2000; ++i) 697 | strings.push_back (helpers::generateRandomString (rndStrSize, resource)); 698 | 699 | CHECK_NOTHROW (strings.clear()); 700 | } 701 | 702 | SECTION ("Mixed container allocations") 703 | { 704 | struct five { char data[5]; }; 705 | struct ninetyNine { char data[99]; }; 706 | struct mixed 707 | { 708 | int w = 99; 709 | char x = 'x'; 710 | std::size_t y = 1; 711 | long long z = 300; 712 | }; 713 | 714 | pmr::vector chars (&resource); 715 | pmr::vector ints (&resource); 716 | pmr::vector fives (&resource); 717 | pmr::vector ninetyNines (&resource); 718 | pmr::vector mixeds (&resource); 719 | pmr::vector strings (&resource); 720 | 721 | auto rndVec = Catch::Generators::random (0, 6); 722 | auto rndChar = Catch::Generators::random (0, (int) std::numeric_limits::max()); 723 | auto rndInt = Catch::Generators::random (0, std::numeric_limits::max()); 724 | auto rndStrSize = Catch::Generators::random (sizeof(pmr::string), 50); 725 | 726 | for (int i = 0; i < 2000; ++i) 727 | { 728 | switch (rndVec.get()) 729 | { 730 | case 0: chars.push_back ((char) rndChar.get()); break; 731 | case 1: ints.push_back (rndInt.get()); break; 732 | case 2: fives.push_back (five()); break; 733 | case 3: ninetyNines.push_back (ninetyNine()); break; 734 | case 4: mixeds.push_back (mixed()); break; 735 | case 5: strings.push_back (helpers::generateRandomString (rndStrSize, resource)); break; 736 | default: continue; 737 | } 738 | 739 | rndVec.next(); 740 | rndChar.next(); 741 | rndInt.next(); 742 | } 743 | 744 | // Touch some random memory to check alignment's correct 745 | // and nothing crashes when we try to access it. 746 | for (auto& m : mixeds) 747 | { 748 | m.w = rndInt.next(), rndInt.get(); 749 | m.y = (std::size_t) m.w; 750 | CAPTURE (m.y); // prevent loop from being optimised out 751 | } 752 | 753 | for (auto& s : strings) 754 | { 755 | s[s.size() / 2] = (char) rndChar.get(); 756 | s[s.size() - 1] = (char) rndChar.get(); 757 | CAPTURE (s); // prevent loop from being optimised out 758 | } 759 | 760 | // Deallocate a lot of blocks 761 | CHECK_NOTHROW (mixeds.clear()); 762 | CHECK_NOTHROW (strings.clear()); 763 | 764 | // A final check that we can allocate more blocks of any size. 765 | std::vector ptrs; 766 | constexpr std::size_t maxSize = 512; 767 | 768 | for (std::size_t i = 1; i < maxSize; ++i) 769 | CHECK_NOTHROW (ptrs.push_back (resource.allocate (i))); 770 | 771 | for (std::size_t i = 0; i < (maxSize - 1); ++i) 772 | CHECK_NOTHROW (resource.deallocate (ptrs[i], i + 1)); 773 | } 774 | 775 | SECTION ("Recovery from memory exhaustion") 776 | { 777 | CAPTURE (typeid (fixture).name()); 778 | std::vector ptrs; 779 | 780 | try 781 | { 782 | while (true) 783 | ptrs.emplace_back (resource.allocate (1024)); 784 | } 785 | catch (const std::bad_alloc&) 786 | { 787 | // It's ok for this to throw again, but we don't want it to crash. 788 | try { ptrs.emplace_back (resource.allocate (1024)); } 789 | catch (const std::bad_alloc&) {} 790 | 791 | // Check we can deallocate after a bad_alloc throw. 792 | for (auto ptr : ptrs) 793 | resource.deallocate (ptr, 1024); 794 | 795 | // Check we can reuse memory after a bad_alloc throw. 796 | if constexpr (! std::is_same_v) 797 | { 798 | const auto numToRetry = ptrs.size(); 799 | ptrs.clear(); 800 | 801 | CHECK_NOTHROW([&] { 802 | for (size_t i = 0; i < numToRetry; ++i) 803 | ptrs.push_back(resource.allocate(1024)); 804 | }); 805 | 806 | for (auto ptr : ptrs) 807 | resource.deallocate (ptr, 1024); 808 | } 809 | } 810 | } 811 | } 812 | 813 | //============================================================================== 814 | TEMPLATE_TEST_CASE ("Pool & Free-List memory re-use", "[memory_resource]", UnsyncPoolFixture, FreeListFixture) 815 | { 816 | namespace pmr = cradle::pmr; 817 | 818 | TestType fixture; 819 | auto& resource = fixture.resource; 820 | 821 | SECTION ("Can re-use blocks without allocating more space") 822 | { 823 | constexpr auto alignment = 4; 824 | auto rndSize = Catch::Generators::random (1, 512); 825 | 826 | auto ptrs = std::unordered_map(); 827 | auto freedBlocks = std::vector(); 828 | 829 | for (std::size_t i = 0; i < helpers::numTestAllocs; ++i, rndSize.next()) 830 | { 831 | const auto size = std::size_t (rndSize.get()); 832 | const auto ptr = resource.allocate (size, alignment); 833 | ptrs.insert ({ptr, size}); 834 | } 835 | 836 | const auto initialCount = fixture.allocationsCount(); 837 | const auto initialSize = fixture.allocationsSize(); 838 | 839 | for (auto it = ptrs.begin(); ptrs.size() > helpers::numTestAllocs/2; it = ptrs.erase (it)) 840 | { 841 | freedBlocks.push_back (it->second); 842 | resource.deallocate (it->first, it->second); 843 | } 844 | 845 | const auto afterDeallocateCount = fixture.allocationsCount(); 846 | const auto afterDeallocateSize = fixture.allocationsSize(); 847 | 848 | // Shuffle so they're not re-allocated in the same order 849 | for (std::size_t i = 0; i < freedBlocks.size(); ++i, rndSize.next()) 850 | std::swap (freedBlocks[i], freedBlocks[rndSize.get()]); 851 | 852 | // Reallocate the same blocks in a different order 853 | for (auto& size : freedBlocks) 854 | ptrs.insert ({resource.allocate (size, alignment), size}); 855 | 856 | CHECK (ptrs.size() == helpers::numTestAllocs); 857 | CHECK (afterDeallocateCount <= initialCount); 858 | CHECK (afterDeallocateSize <= initialSize); 859 | CHECK (initialCount <= fixture.allocationsCount()); 860 | CHECK (initialSize <= fixture.allocationsSize()); 861 | 862 | for (auto& pair : ptrs) 863 | resource.deallocate (pair.first, pair.second, alignment); 864 | } 865 | } 866 | 867 | //============================================================================== 868 | /** Special memory overhead characteristics of unsynchronized_pool_resource. */ 869 | TEST_CASE ("unsynchronized_pool_resource overhead limits", "[memory_resource]") 870 | { 871 | namespace pmr = cradle::pmr; 872 | 873 | tracking_memory_resource src; 874 | pmr::unsynchronized_pool_resource resource {&src}; 875 | 876 | SECTION ("Median overhead for book-keeping and pre-allocation is low") 877 | { 878 | // We only measure overhead for block sizes greater than fundamental 879 | // alignment. We can't expect low overhead for sizes less than that. 880 | const auto minNumBytes = alignof (std::max_align_t); 881 | const auto numBytes = GENERATE_COPY (as(), minNumBytes, 19, 32, 33, 170, 256); 882 | constexpr auto alignment = 4; 883 | 884 | // The overheads vary at different points in time, depending on 885 | // internal pre-reserved memory, whether the allocations fit 886 | // snugly into a block etc, so we will take the median overhead, 887 | // and we allow a larger overhead value for irregular sizes. 888 | const auto maxMedianOverhead = (numBytes % 8 == 0) ? 2.0 : 3.0; 889 | auto ptrs = std::unordered_set(); 890 | auto overheads = std::vector(); 891 | 892 | for (std::size_t i = 0; i < helpers::numTestAllocs; ++i) 893 | { 894 | ptrs.insert (resource.allocate (numBytes, alignment)); 895 | overheads.push_back (double (src.total_allocated()) / double (numBytes * (i + 1))); 896 | } 897 | 898 | std::sort (overheads.begin(), overheads.end(), std::less<>()); 899 | 900 | const auto min = overheads[0]; 901 | const auto median = overheads[helpers::numTestAllocs/2]; 902 | const auto max = overheads.back(); 903 | 904 | CAPTURE (numBytes, min, median, max); 905 | CHECK (median < maxMedianOverhead); 906 | 907 | for (auto& ptr : ptrs) 908 | resource.deallocate (ptr, numBytes, alignment); 909 | } 910 | } 911 | 912 | PMR_DIAGNOSTIC_POP 913 | --------------------------------------------------------------------------------