diff --git a/libuavcan/include/uavcan/helpers/heap_based_pool_allocator.hpp b/libuavcan/include/uavcan/helpers/heap_based_pool_allocator.hpp new file mode 100644 index 0000000000..a012ee2c9a --- /dev/null +++ b/libuavcan/include/uavcan/helpers/heap_based_pool_allocator.hpp @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2015 Pavel Kirienko + */ + +#ifndef UAVCAN_HELPERS_HEAP_BASED_POOL_ALLOCATOR_HPP_INCLUDED +#define UAVCAN_HELPERS_HEAP_BASED_POOL_ALLOCATOR_HPP_INCLUDED + +#include +#include + +namespace uavcan +{ +/** + * A special-purpose implementation of a pool allocator that keeps the pool in the heap using malloc()/free(). + * The pool grows dynamically, ad-hoc, thus using as little memory as possible. + * + * Allocated blocks will not be freed back automatically, but there are two ways to force their deallocation: + * - Call @ref shrink() - this method frees all blocks that are unused at the moment. + * - Destroy the object - the desctructor calls @ref shrink(). + * + * All operations are thread-safe, the safety is obtained through the standard atomic compare-and-swap operator (CAS). + * If thread safety is not required, the CAS operator can be replaced with a dummy version that simply performs the + * assignment without any checks. + * If thread safety is required, but the hardware does not support atomic exchange operations, CAS can be emulated + * with a mutex or a critical section roughly as follows: + * + * bool softwareAtomicCompareAndSwap(void** address, void* expected, void* replacement) + * { + * RaiiSynchronizationPrimitive lock; + * if (*address != expected) + * { + * return false; + * } + * *address = replacement; + * return true; + * } + */ +template +class UAVCAN_EXPORT HeapBasedPoolAllocator : public IPoolAllocator, Noncopyable +{ + union Node + { + Node* next; + private: + uint8_t data[BlockSize]; + long double _aligner1; + long long _aligner2; + }; + + Node* volatile cache_; + + Node* popCache() + { + // http://www.ibm.com/developerworks/aix/library/au-multithreaded_structures2/ + for (;;) + { + Node* volatile const result = cache_; + if (result == NULL) + { + break; + } + if ((cache_ != NULL) && + AtomicCompareAndSwap(reinterpret_cast(const_cast(&cache_)), result, result->next)) + { + return result; + } + } + return NULL; + } + +public: + /** + * The allocator initializes with empty cache, so first allocations will be served from heap. + */ + HeapBasedPoolAllocator() : cache_(NULL) { } + + /** + * The destructor de-allocates all blocks that are currently in the cache. + * BLOCKS THAT ARE CURRENTLY HELD BY THE APPLICATION WILL LEAK. + */ + ~HeapBasedPoolAllocator() { shrink(); } + + /** + * Takes a block from the cache, unless it's empty. + * In the latter case, allocates a new block in the heap. + */ + virtual void* allocate(std::size_t size) + { + if (size > BlockSize) + { + return NULL; + } + if (Node* n = popCache()) + { + return n; + } + else + { + return std::malloc(sizeof(Node)); + } + } + + /** + * Puts the block back to cache. + * The block will not be free()d automatically; see @ref shrink(). + */ + virtual void deallocate(const void* ptr) + { + if (ptr == NULL) + { + return; + } + + Node* volatile const n = static_cast(const_cast(ptr)); + do + { + n->next = cache_; + } + while (!AtomicCompareAndSwap(reinterpret_cast(const_cast(&cache_)), n->next, n)); + } + + /** + * Heap-based pool is virutally infinite in size, so this method just returns maximum possible number of blocks. + */ + virtual uint16_t getNumBlocks() const { return NumericTraits::max(); } + + /** + * Frees all blocks that are not in use at the moment. + */ + void shrink() + { + while (Node* p = popCache()) + { + std::free(p); + } + } + + /** + * This function naively counts the number of cached blocks. + * It is not thread-safe and is mostly designed for testing and debugging purposes. + * Don't use it in production code. + */ + unsigned getNumCachedBlocks() + { + unsigned out = 0; + Node* p = cache_; + while (p != NULL) + { + out++; + p = p->next; + } + return out; + } +}; + +} + +#endif diff --git a/libuavcan/test/helpers/heap_based_pool_allocator.cpp b/libuavcan/test/helpers/heap_based_pool_allocator.cpp new file mode 100644 index 0000000000..93da37130f --- /dev/null +++ b/libuavcan/test/helpers/heap_based_pool_allocator.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 Pavel Kirienko + */ + +#include +#include + +inline bool atomicCompareAndSwap(void** address, void* expected, void* replacement) +{ + return __sync_bool_compare_and_swap(address, expected, replacement); +} + +TEST(ThreadSafeHeapBasedPoolAllocator, Basic) +{ + uavcan::HeapBasedPoolAllocator allocator; + + ASSERT_EQ(0, allocator.getNumCachedBlocks()); + + void* a = allocator.allocate(10); + void* b = allocator.allocate(10); + void* c = allocator.allocate(10); + void* d = allocator.allocate(10); + + ASSERT_EQ(0, allocator.getNumCachedBlocks()); + + allocator.deallocate(a); + ASSERT_EQ(1, allocator.getNumCachedBlocks()); + + allocator.deallocate(b); + ASSERT_EQ(2, allocator.getNumCachedBlocks()); + + allocator.deallocate(c); + ASSERT_EQ(3, allocator.getNumCachedBlocks()); + + a = allocator.allocate(10); + ASSERT_EQ(2, allocator.getNumCachedBlocks()); + ASSERT_EQ(c, a); + + allocator.deallocate(a); + ASSERT_EQ(3, allocator.getNumCachedBlocks()); + + allocator.shrink(); + ASSERT_EQ(0, allocator.getNumCachedBlocks()); + + allocator.deallocate(d); + ASSERT_EQ(1, allocator.getNumCachedBlocks()); + + allocator.shrink(); + ASSERT_EQ(0, allocator.getNumCachedBlocks()); +}