├── .gitattributes ├── LICENSE ├── README.md ├── benchmark-27.11.21.png └── linear_pool_allocator.hh /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Michael Brown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linear Pool Allocator 2 | The Linear Pool Allocator is an attempt to combine the benefits of: 3 | * a Linear Fixed Size Allocator - A contiguous sequence of fixed sized blocks, with a monotonically incrementing index representing the current available blocks. 4 | * and a Pool Allocator - A contiguous sequence of fixed sized blocks with a linked list embedded into the free blocks, and a head pointer that holds the currently available blocks. 5 | 6 | The shortcomings of a Linear Allocator is the lack of ability to deallocate and reuse blocks. 7 | The shortcomings of a Pool Allocator is the initial initialisation of the linked list of free blocks, and the allocation cost is higher when compared to a Linear Allocator. 8 | 9 | The Linear Pool Allocator works by treating all first allocations of a block as a Linear allocation. When a block is deallocated, that block is then added to the head of the pool allocator side of the allocator. This means there is no upfront construction of the pool allocators linked list, and first time allocations are always done via the faster linear allocation. 10 | 11 | ## Use Case 12 | The primary designed use case is to be used allocating tree structures, where the majority of nodes are created at initial construction and transformations are performed on that structure. E.g. Reading in a file and constructing a tree from that data. This allocator leverages the speed of a linear allocator when the bulk of the nodes are created, and still allows the possibility of deallocations when manipulating the tree. 13 | 14 | This allocator is designed to be composed with other allocator compontents (Segreator, Bucketizer etc) that manage multiple allocators to enable expanding, condensing, and policies of allocations. 15 | 16 | ## Algorithm 17 | struct slot_node - A structure of a free slot for the pool allocator 18 | 19 | block_t pool[pool_size] - An array of blocks, uninitialised 20 | block_t* head{ &pool[pool_size] } - Head is a pointer to the last element of the pool (This is a decrementing linear allocator). 21 | block_t** tail{ &head } - This is a pointer to either the head variable, or a ->next variable within a free slot node. 22 | 23 | void* allocate() / void dellocate(void*) 24 | * **Linear Mode** 25 | If tail is equal to the address of head - We know that the pool allocator has no free slots in the linked list, and we use the linear allocation method. In this mode, the value of head is the linear allocation monotonic index. 26 | 27 | * **Pool Mode** 28 | When a block is deallocated, We perform the normal pool allocation steps. The monotonic index value is set into the first free slot's ->next value. The value of tail is then set to the address of ->next. Further deallocations does not move this value as the linked list is First In, Last Out. 29 | 30 | When in Pool Mode, an allocation will default to the Pool allocation strategy. The first free slot is selected, head is set to the value of ->next, and that slot is returned. If tail was pointing to that slots ->next variable, tail is set to head - returning to Linear Mode. 31 | 32 | void* allocate_linear() 33 | * **Forced Linear Mode** 34 | The monotonic index value is always available via the tail pointer. This means you can override the use of the pool allocation strategy and always perform a linear allocation. This can potentially increase fragmentation, but provide guarantees on the performance and guarantee linear memory is returned. 35 | 36 | ## Benchmark 37 | The benchmark is X speed up compared against my standard Pool Allocator. This is by no means a thorough benchmark - My Pool Allocator that I compare against might not be the fastest. 38 | 39 | The Y axis is the number of blocks the allocator manages - All blocks of 8 bytes in size. 40 | 41 | This benchmark shows the allocation and deallocation of all nodes in sequence. An additional "wipe" deallocation method is also supplied to deallocate all nodes at once. 42 | 43 | ![Benchmark](https://github.com/mikey-b/lib/blob/main/benchmark-27.11.21.png?raw=true "Benchmark") 44 | 45 | ### Results 46 | 47 | The default strategy of Linear Pool Allocator is slower than the default Pool Allocator (red vs blue bars), until pool size is > 32k. While I would like to get this smaller, I am not surprised by this. I also suspect the cache is helping the standard pool allocator here - the small node sizes and allocation style of the benchmark is most likely showing the worst case difference. 48 | 49 | The benefit of this allocator is to be able to utilise the linear allocator behaviour when a tree is initially being constructed - when most allocations occur. 50 | 51 | Using the linear allocation strategy enables a 1.5x - 2x speed up over a pool allocator. This is most likely slower than a standard linear allocator (not measured here) - But provides the possibility to deallocate and reuse. 52 | 53 | ## Next 54 | I have no interest in writing or maintaining a library. By all means, if you see value in this, you are welcome to it. -------------------------------------------------------------------------------- /benchmark-27.11.21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikey-b/linear_pool_allocator/dabda0bacbd88d931341385e210081a535fd1316/benchmark-27.11.21.png -------------------------------------------------------------------------------- /linear_pool_allocator.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | template struct linear_pool_allocator { 6 | private: 7 | using block_t = typename std::aligned_storage::type; 8 | 9 | struct slot_node { 10 | slot_node* next; 11 | }; 12 | static_assert(sizeof(slot_node) <= sizeof(block_t)); 13 | 14 | block_t* head{ &pool[pool_size] }; // This is correct! Always starts as a linear allocator and to the RIGHT of the last element 15 | block_t** tail{ &head }; 16 | block_t pool[pool_size]; 17 | 18 | public: 19 | template 20 | [[gnu::malloc]] void* allocate_linear() { 21 | if constexpr (isInitialConstruction) { 22 | // We can remove the indirection if we can guarentee that there has never 23 | // been a dealloation yet. This is now identical to a bump allocator. 24 | // assert(*tail == head); 25 | if (head == &pool[0]) return nullptr; 26 | head -= 1; 27 | return reinterpret_cast(head); 28 | } else { 29 | if (*tail == &pool[0]) return nullptr; 30 | *tail -= 1; 31 | return reinterpret_cast(*tail); 32 | } 33 | } 34 | 35 | [[gnu::malloc]] void* allocate() { 36 | if (head == &pool[0]) return nullptr; 37 | 38 | if (tail == &head) { 39 | head -= 1; 40 | return reinterpret_cast(head); 41 | } else { 42 | auto node = reinterpret_cast(head); 43 | if (reinterpret_cast(tail) == &node->next) { 44 | tail = &head; 45 | } 46 | head = reinterpret_cast(node->next); 47 | return reinterpret_cast(node); 48 | } 49 | } 50 | 51 | [[gnu::nonnull]] void deallocate(void* blk) { 52 | if (*tail == reinterpret_cast(blk)) { 53 | *tail += 1; 54 | } else { 55 | auto node = reinterpret_cast(blk); 56 | node->next = reinterpret_cast(head); 57 | if (tail == &head) tail = reinterpret_cast(&node->next); 58 | head = reinterpret_cast(blk); 59 | } 60 | } 61 | 62 | void deallocateAll() { 63 | head = &pool[pool_size]; // This is correct! Always starts as a linear allocator and to the RIGHT of the last element 64 | tail = &head; 65 | } 66 | }; --------------------------------------------------------------------------------