├── README.md └── scope_stack_alloc.h /README.md: -------------------------------------------------------------------------------- 1 | # scope_stack_alloc 2 | 3 | Scope stacks are a memory management tool allowing for a linear-memory layout 4 | of arbitrary object hierarchies. 5 | 6 | ### Why 7 | Many reasons 8 | 9 | ## Heap allocation is expensive 10 | * Complicated to manage 11 | * Bad cache locality 12 | 13 | ## Fragmentation 14 | Most of the time you allocate objects with mixed life-times next to each other, 15 | this leads to fragmentation 16 | * Each allocation has to traverse a *free-list* 17 | * Cache miss for each probed location 18 | * Large blocks disappear quickly 19 | 20 | ## Alternatives don't work 21 | Linear allocators can over come this but they have a variety of limitations 22 | * Can't deal with resource cleanup 23 | * Need manual cleanup functions 24 | * Manually remember what resources to free 25 | * Error prone 26 | * RAII not possible 27 | * Tedious low-level boilerplate 28 | 29 | Scope stacks overcome all these limitations 30 | * Is a linear allocator (backed by stack space) 31 | * Rewinds when a scope is left (calling destructors.) 32 | * Only allocates from topmost scope 33 | * Can rewind scopes as desired 34 | * Fine-grain control over nested object lifetimes 35 | * Safe `setjmp` and `longjmp` (won't leak objects.) 36 | * Easy thread-local scratch space 37 | * PIMPL idiom becomes more attractive 38 | * Faster too 39 | 40 | ### Using 41 | Just include *"scope_stack_alloc.h"* and make a stack object. 42 | 43 | ### Example 44 | 45 | #include "scope_stack_alloc.h" 46 | 47 | static stack<(1<<20)> gStack; // 1MB 48 | 49 | int main() { 50 | gStack || [&]() { // Or gStack.enter(); 51 | auto &it = gStack.acquire>(); 52 | // Or acquire>(gStack); 53 | it.push_back(100); 54 | /// ... 55 | }; // Implicit gStack.leave() here 56 | } 57 | 58 | ### Limitations 59 | There are some limitations 60 | * Must know all object lifetimes and ownership when acquiring 61 | * Cannot hold onto pointers to acquired memory 62 | * Think of it like local objects in a function 63 | * Memory is no longer valid when frame is left 64 | 65 | ### Frames 66 | The manual scope guard function `enter` returns a *frame-index* which can 67 | be passed to `leave` to leave that scope. Similarly; `cleanup` optionally takes 68 | a *frame-index*, all active frames from **one-past** this index passed will be 69 | purged (all active objects of those frames will be destroyed.) This is useful for 70 | quick *tear-down* situations where the approprate calls to `leave` were not 71 | made causing the frame to leak resources (`longjmp` for example.) 72 | 73 | ### Resources 74 | https://www.ea.com/frostbite/news/scope-stack-allocation 75 | -------------------------------------------------------------------------------- /scope_stack_alloc.h: -------------------------------------------------------------------------------- 1 | #ifndef SCOPE_STACK_ALLOC_H 2 | #define SCOPE_STACK_ALLOC_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | template 9 | struct stack; 10 | 11 | namespace detail { 12 | 13 | template 14 | struct destructor { 15 | static void dtor(void *self); 16 | }; 17 | 18 | class object { 19 | friend struct frame; 20 | 21 | template 22 | friend struct ::stack; 23 | 24 | object(void *self, void (*dtor)(void*)); 25 | void *self; 26 | void (*dtor)(void *); 27 | void destroy(); 28 | }; 29 | 30 | class frame { 31 | template 32 | friend struct ::stack; 33 | 34 | frame(size_t index, unsigned char *sp); 35 | unsigned char *sp; 36 | std::size_t index; 37 | std::vector objects; 38 | void cleanup(); 39 | }; 40 | 41 | template 42 | void destructor::dtor(void *self) { 43 | reinterpret_cast(self)->~T(); 44 | } 45 | 46 | ///! object 47 | inline object::object(void *self, void (*dtor)(void*)) 48 | : self(self) 49 | , dtor(dtor) 50 | { 51 | } 52 | 53 | inline void object::destroy() { 54 | dtor(self); 55 | } 56 | 57 | ///! frame 58 | inline frame::frame(std::size_t index, unsigned char *sp) 59 | : index(index) 60 | , sp(sp) 61 | { 62 | } 63 | 64 | inline void frame::cleanup() { 65 | for (auto it = objects.rbegin(); it != objects.rend(); ++it) 66 | (*it).destroy(); 67 | } 68 | 69 | } 70 | 71 | template 72 | struct stack { 73 | stack() : sp(data) { } 74 | 75 | std::size_t enter() { 76 | frames.push_back(new detail::frame(frames.size(), sp)); 77 | return frames.size(); 78 | } 79 | 80 | void leave(std::size_t f = 0) { 81 | if (f >= frames.size()) return; 82 | detail::frame *get = frames[f]; 83 | get->cleanup(); 84 | frames.erase(frames.begin() + f); 85 | delete get; 86 | } 87 | 88 | unsigned char *allocate(size_t size, size_t alignment) { 89 | unsigned char *where = sp; 90 | const size_t base = reinterpret_cast(where); 91 | sp += size + alignment; 92 | return reinterpret_cast(base + (alignment - base % alignment)); 93 | } 94 | 95 | template 96 | T &acquire() { 97 | T *obj = new(allocate(sizeof(T), alignof(T))) T(); 98 | frames.back()->objects.push_back({static_cast(obj), &detail::destructor::dtor}); 99 | return *obj; 100 | } 101 | 102 | template 103 | T &acquire(Ts... ts) { 104 | T *obj = new(allocate(sizeof(T), alignof(T))) T(std::forward(ts...)); 105 | frames.back()->objects.push_back({static_cast(obj), &detail::destructor::dtor}); 106 | return *obj; 107 | } 108 | 109 | void cleanup(std::size_t f = 0) { 110 | if (f + 1 >= frames.size()) return; 111 | for (auto it = frames.rbegin(); it != frames.rend() + f - 1; ++it) { 112 | (*it)->cleanup(); 113 | delete *it; 114 | } 115 | frames.erase(frames.begin() + f + 1, frames.end()); 116 | sp = frames[f]->sp; 117 | } 118 | 119 | stack &operator || (std::function scope) { 120 | enter(); 121 | scope(); 122 | leave(); 123 | return *this; 124 | } 125 | 126 | private: 127 | std::vector frames; 128 | alignas(alignof(void*)) unsigned char data[E]; 129 | unsigned char *sp; 130 | }; 131 | 132 | // Utility 133 | template 134 | T &acquire(stack &s) { 135 | return s.template acquire(); 136 | } 137 | 138 | #endif 139 | --------------------------------------------------------------------------------