diff --git a/include/Engine/Core/MemoryPool.h b/include/Engine/Core/MemoryPool.h new file mode 100644 index 00000000..246f7d0a --- /dev/null +++ b/include/Engine/Core/MemoryPool.h @@ -0,0 +1,294 @@ +#ifndef MemoryPool_h__ +#define MemoryPool_h__ +#include "Common.h" + +template +class MemoryPoolForwardIterator; + +//This is the class to use if you want to allocate blocks (slots) of raw memory, with a fixed maximum size (stride). +//Additionally, if you know that every memory-block will contain one object of a specific type, (i.e. the stride for the slot +//will the size of the object type) you should use ObjectPool instead, your life will become easier. +//The memory returned by pool.Allocate() has has no type-information +//Works similarly to malloc(), but it allocates a number of slots of a certain size. +//Basically, pool.Allocate() does the same as malloc(m_Stride). +// +//When the pool goes out of scope (when it is destructed) it will free all memory that was allocated automatically. +// +//The template input type T does not affect the structure of the memory pool, it will +//only affect how the iterators access the pool data, for example: +//MemoryPool p; MemoryPool::iterator iter = p.begin(); +//Here, *iter will return the data in the first allocated slot in the pool, interpreted as an int. +//If you only intend to use the pointers returned by Allocate(), and not iterate through the pool, +//T is never used and can safely be set to anything. e.g. char. +template +class MemoryPool +{ + template + friend class MemoryPoolForwardIterator; +public: + typedef MemoryPoolForwardIterator iterator; + typedef ptrdiff_t difference_type; + typedef size_t size_type; + typedef T value_type; + typedef T* pointer; + typedef T& reference; + MemoryPool() + : m_StartAddress(nullptr) + , m_SlotIsAllocated() + , m_NumSlots(0) + , m_Stride(0) + , m_NumAllocatedSlots(0) + , m_LowestAllocatedSlot(0) + { } + + //[stride] is the number of bytes allocated per slot in the pool, + //the smallest amount of memory you can get from Allocate(). (Usually, the maximum size of an object to allocate.) + //[numMaxElements] is the number of slots in the pool. That is, how many stride-sized memory blocks + //the pool will have. (Usually, how many objects can be allocated.) + //These parameters are used to preallocate memory for efficency. + //If numMaxElements is exceeded, the pool will start allocate memory dynamically, "outside the pool", and is much slower. + MemoryPool(size_t numMaxElements, size_t stride) + : m_StartAddress(new char[numMaxElements*stride]) + , m_SlotIsAllocated(numMaxElements, false) + , m_NumSlots(numMaxElements) + , m_Stride(stride) + , m_NumAllocatedSlots(0) + , m_CurrentAllocSlot(0) + , m_LowestAllocatedSlot(m_NumSlots) + { } + + //We may get problems with memory being released + //prematurely, etc. if we allow copies. + MemoryPool(const MemoryPool& other) = delete; + MemoryPool(const MemoryPool&& other) = delete; + + //Free all memory that has been allocated. + ~MemoryPool() + { + if (m_StartAddress != nullptr) { + delete[] m_StartAddress; + m_StartAddress = nullptr; + } + for (char* addr : m_ExtraMemory) + free(addr); + m_ExtraMemory.clear(); + } + + //Allocates space for a contingous block of memory the size of [stride] bytes, and returns a pointer to the data. + //The data is not touched, and remains uninitialized, i.e. it will have random values in it. + //If element cannot be allocated in the pool, because the memory ran out, memory is allocated dynamically with malloc() "outside the pool". + char* Allocate() + { + for (; m_CurrentAllocSlot < m_NumSlots && m_SlotIsAllocated[m_CurrentAllocSlot]; ++m_CurrentAllocSlot); + if (m_CurrentAllocSlot < m_NumSlots) { + if (m_LowestAllocatedSlot > m_CurrentAllocSlot) + m_LowestAllocatedSlot = m_CurrentAllocSlot; + //Mark the slot as allocated. + m_SlotIsAllocated[m_CurrentAllocSlot] = true; + ++m_NumAllocatedSlots; + //Also increment slot to allocate. + return m_StartAddress + m_Stride*m_CurrentAllocSlot++; + } + else { + m_ExtraMemory.push_back((char*)malloc(m_Stride)); + //We should preferably not enter here to avoid performance issues. Set more numMaxElements in constructor instead. + LOG_WARNING("Allocated slots exceed Pool size, extra memory allocated dynamically. Pool size: %u, dynamic size: %u.", m_NumSlots, m_ExtraMemory.size()); + return m_ExtraMemory.back(); + } + } + + //Free memory that was allocated earlier with Allocate(). + void Free(char* obj) + { + //If memory was allocated in the memory pool. + //We are guaranteed to enter here if we have zero malloc() allocations, + //or if the obj was allocated in the pool, + //however, comparisons is technically undefined + //(i.e. IsAllocatedInPool may give false positives) + //if it was malloc():ed + //so, we may enter here even if we shouldn't. + if (IsAllocatedInPool(obj)) { + --m_NumAllocatedSlots; + const size_t freeSlot = (obj - m_StartAddress) / m_Stride; + m_SlotIsAllocated[freeSlot] = false; + if (freeSlot < m_CurrentAllocSlot) + m_CurrentAllocSlot = freeSlot; + //If we happened to remove the begin() slot, find the next one. + //Increment until we find an allocated slot, or go out of bounds. + while (m_LowestAllocatedSlot != m_NumSlots && !m_SlotIsAllocated[m_LowestAllocatedSlot]) + ++m_LowestAllocatedSlot; + } + //If memory was allocated dynamically with malloc because we didn't have enough storage in pool. + //I.e: if numMallocs > 0 and obj is outside [m_StartAddress-->m_NumSlots]. + else { + m_ExtraMemory.erase(find(m_ExtraMemory.begin(), m_ExtraMemory.end(), obj)); + free(obj); + } + } + + //Returns an iterator pointing to the first element. + //If pool is empty it will be equal to end() and shall not be dereferenced. + iterator begin() const + { + //If pool is empty, lowest slot will be numSlots, + //and m_ExtraMemory.size will be 0, so begin == end. + return iterator(this, m_LowestAllocatedSlot); + } + + //Returns an iterator pointing beyond the last element. + //This shall not be dereferenced (Gives a run-time error). + iterator end() const + { + return iterator(this, m_NumSlots + m_ExtraMemory.size()); + } + + //Returns true iff the pool has no allocated elements. + bool empty() const + { + return m_LowestAllocatedSlot == m_NumSlots && m_ExtraMemory.empty(); + } + + //Returns the total number of allocated elements. + size_t size() const + { + return m_NumAllocatedSlots + m_ExtraMemory.size(); + } + + //Returns the number of elements allocated inside the pool boundary. + size_t PoolSize() const + { + return m_NumAllocatedSlots; + } + + //Returns the number of elements allocated outside the pool boundary (in dynamic extra space). + size_t ExtraSize() const + { + return m_ExtraMemory.size(); + } + + //Dumps information about what the pool memory looks like right now + //into an output stream (e.g. file/std::cout, anything that has an operator<<) + //Interpret the data in the memory as InterpretType. + template + void Dump(OutStream& out) const + { + out << "Primary pool memory: Allocated Slots=" << m_NumAllocatedSlots << std::endl; + for (size_t i = 0; i < m_NumSlots; ++i) { + out << "Slot nr " << i << ": "; + for (size_t c = 0; c < m_Stride/sizeof(InterpretType); ++c) + out << (*reinterpret_cast(m_StartAddress + i*m_Stride + c)) << "\t"; + out << "Allocated = " << m_SlotIsAllocated[i] << std::endl; + } + out << "Dynamic extra pool memory: Slots=" << m_ExtraMemory.size() << std::endl; + for (size_t i = 0; i < m_ExtraMemory.size(); ++i) { + out << "Extra " << i << ": "; + for (size_t c = 0; c < m_Stride / sizeof(InterpretType); ++c) + out << (*reinterpret_cast(m_ExtraMemory[i] + c)) << "\t"; + out << std::endl; + } + } + + //Dumps information about what the pool memory looks like right now + //into std::cout. Interpret the data in the memory as InterpretType. + template + void Dump() const + { + Dump(std::cout); + } + +private: + char* m_StartAddress; + std::vector m_SlotIsAllocated; + std::vector m_ExtraMemory; + size_t m_NumSlots; + size_t m_LowestAllocatedSlot; + size_t m_NumAllocatedSlots; + size_t m_Stride; + size_t m_CurrentAllocSlot; + bool IsAllocatedInPool(char* p) + { + //If we went outside the pool limits and used dynamic allocation. + if (!m_ExtraMemory.empty()) + { + //Assumes that std::uintptr_t is a thing here. No idea what linux does here. +#ifdef UINTPTR_MAX + //Assumes the memory has a linear address space, no weird jumps. Should work on modern platforms. + //Assumes unsigned values loop back to very positive when they go negative. Standard C++ behavior. + return (reinterpret_cast(p) - reinterpret_cast(m_StartAddress) < m_NumSlots * m_Stride); +#else + //Fallback on inefficient loop otherwise. + for (size_t i = 0; i < m_NumSlots; i++) + if (p == m_StartAddress + i * m_Stride) + return true; + return false; +#endif + } + return true; //If we don't go outside the pool boundary, this will always be true. + } +}; + +template +class MemoryPoolForwardIterator + : public std::iterator +{ +public: + MemoryPoolForwardIterator(const MemoryPool* pool, size_t slotPos) + : m_Pool(pool) + , m_Pos(slotPos) + { } + + MemoryPoolForwardIterator(const MemoryPoolForwardIterator& other) = default; + MemoryPoolForwardIterator(MemoryPoolForwardIterator&& other) = default; + ~MemoryPoolForwardIterator() = default; + MemoryPoolForwardIterator& operator= (const MemoryPoolForwardIterator& other) = default; + + //Prefix increment i.e. ++iter. More efficient than post increment. + MemoryPoolForwardIterator& operator++() + { + //Increment position, if pos is in the pool area and the slot isn't allocated, + //keep checking the next position. + while (++m_Pos < m_Pool->m_NumSlots && !m_Pool->m_SlotIsAllocated[m_Pos]); + return *this; + } + + //Postfix increment i.e. iter++. Prefer pre-increment (++iter) for efficiency. + MemoryPoolForwardIterator& operator++(int) + { + MemoryPoolForwardIterator copyIter(*this); + operator++(); + return copyIter; + } + + bool operator!= (const MemoryPoolForwardIterator& other) const + { + return m_Pos != other.m_Pos; + } + + bool operator== (const MemoryPoolForwardIterator& other) const + { + return m_Pos == other.m_Pos; + } + + T& operator* () const + { + return *this->operator->(); + } + + T* operator-> () const + { + //If pos < numSlots then the iterator is in the pool. + //Else it is in the extra memory. + + //NOTE: If you get a "vector subscript out of range" error here, + //then you possibly dereferenced the end() iterator (don't do that). + return (m_Pos < m_Pool->m_NumSlots) + ? (reinterpret_cast(m_Pool->m_StartAddress + m_Pool->m_Stride*m_Pos)) + : (reinterpret_cast(m_Pool->m_ExtraMemory[m_Pos - m_Pool->m_NumSlots])); + } + +private: + const MemoryPool* m_Pool; + size_t m_Pos; +}; + +#endif \ No newline at end of file diff --git a/include/Engine/Core/ObjectPool.h b/include/Engine/Core/ObjectPool.h new file mode 100644 index 00000000..d9e7d3c7 --- /dev/null +++ b/include/Engine/Core/ObjectPool.h @@ -0,0 +1,191 @@ +#ifndef ObjectPool_h__ +#define ObjectPool_h__ + +#include "MemoryPool.h" + +// - Example usage code below class definition. +// +//This is the class to use if you want to dynamically allocate memory to hold a specific object type. +// +//When the pool goes out of scope (when it is destructed) it destruct all allocated objects and free all memory that was allocated in the pool. +template +class ObjectPool +{ +public: + typedef MemoryPoolForwardIterator iterator; + typedef ptrdiff_t difference_type; + typedef size_t size_type; + typedef T value_type; + typedef T* pointer; + typedef T& reference; + + ObjectPool() + : m_Pool() + { } + + //[numMaxElements] is the number of objects in the pool. This parameter is used to preallocate memory. + //If numMaxElements is exceeded, the pool will start allocate memory dynamically, "outside the pool", which is much slower. + ObjectPool(size_t numMaxElements) + : m_Pool(numMaxElements, sizeof(T)) + { } + + //Destruct all elements in the pool that are still allocated. + //Lastly, m_Pool will go out of scope and destruct, freeing all the memory. + ~ObjectPool() + { + for (T &o : *this) + o.~T(); + } + + //Allocates memory to hold one T object, constructs an object + //by supplying any arguments to T(...) constructor. + //Then returns a pointer to its memory. + // + //Works similiarly to new below: + //T* objPointer = new T(...); //<-- + // ... + //delete objPointer; + template + T* New(Arguments... args) + { + return new (m_Pool.Allocate()) T(args...); + } + + //Calls the destructor of the object pointed to by input parameter. + //Then frees the memory. + //Works similiarly to delete below: + //T* objPointer = new T(...); + // ... + //delete objPointer; //<-- + void Delete(T* pObject) + { + pObject->~T(); + m_Pool.Free(reinterpret_cast(pObject)); + } + + //Calls the destructor of the object pointed to by iterator. + //Then frees the memory. + void Delete(iterator objIterator) + { + pool.Delete(&(*objIterator)); + } + + //Returns an iterator pointing to the first element. + //If pool is empty it will be equal to end() and shall not be dereferenced. + iterator begin() const + { + return m_Pool.begin(); + } + + //Returns an iterator pointing beyond the last element. + //This shall not be dereferenced (Gives a run-time error). + iterator end() const + { + return m_Pool.end(); + } + + //Returns true iff the pool has no allocated elements. + bool empty() const + { + return m_Pool.empty(); + } + + //Returns the total number of allocated elements. + size_t size() const + { + return m_Pool.size(); + } + + //Returns the number of allocated elements but not from dynamically allocated extra space. + size_t PoolSize() const + { + return m_Pool.PoolSize(); + } + + //Returns the number of elements allocated outside the pool boundary (in dynamic extra space). + size_t ExtraSize() const + { + return m_Pool.ExtraSize(); + } + + //Dumps information about what the pool memory looks like right now + //into an output stream (e.g. file/std::cout, anything that has an operator<<) + //Interpret the data in the memory as InterpretType. + template + void Dump(OutStream& out) const + { + m_Pool.Dump(out); + } + + //Dumps information about what the pool memory looks like right now + //into std::cout. Interpret the data in the memory as InterpretType. + template + void Dump() const + { + m_Pool.Dump(); + } +private: + MemoryPool m_Pool; +}; + + +//------------Simple use case example code scenario----------------- +// +// //User defined type that the pool will hold. +// struct HappyStruct { +// int i; +// float f; +// HappyStruct() = default; +// HappyStruct(int ii, float ff) : i(ii), f(ff) { } +// bool operator == (const HappyStruct& o) //std::find wants a == to search through the pool. +// { +// return o.i == i && o.f == f; +// } +// }; +// +// //The pool can hold 100 HappyStruct's, before it starts to struggle and we get performance issues. +// ObjectPool pool(100); +// +// //For some reason, we want to allocate a struct dynamically. +// //Usually, you would do this: HappyStruct* s = new HappyStruct(17, 2.4142f); +// //Instead: +// HappyStruct* pHappy = pool.New(17, 2.4142f); +// +// //If you want to be able to free the memory manually (e.g. for temporary objects), +// //to free space for other objects, save the return value in pHappy and +// //when it is not needed anymore release it back to the pool. +// //Usually, you would do this: delete pHappy; +// //Instead: +// pool.Delete(pHappy); +// +// //Objects that are not deleted manually will be destructed and deallocated automatically when the pool +// //destructs, so elements can be added to the pool like this. +// pool.New(1, 0.0f); +// pool.New(2, 0.0f); +// pool.New(3, 0.0f); +// pool.New(5, 0.0f); +// pool.New(4, 0.0f); +// +// //You can use some standard functions that operate on iterators, +// //for example if you really want to destroy the element with a 5. (Less efficient (it loops) than deleting the pointer directly, as above) +// ObjectPool::iterator it = find(pool.begin(), pool.end(), S(5, 0)); //Find the first element that is equal to S(5,0) +// pool.Delete(it); +// +// //You can iterate through all allocated elements in the pool, like you would through a collection such as vector: +// //Version with awesome C++11 range-based syntax: +// for (auto& element : pool) +// element.i += 2; +// //Version with standard looping: +// for (auto& iter = pool.begin(); iter != pool.end(); ++iter) +// iter->i += 2; +// +// //Output a snapshot of the memory in the pool, with the memory data interpreted as int, to a stream. +// std::ostream& out = std::cout; +// pool.Dump(out); +// //Output a snapshot of the memory in the pool, with the memory data interpreted as char (bytedata), to a file. +// std::ofstream file("randomFile.txt"); +// pool.Dump(file); +// file.close(); +// + +#endif \ No newline at end of file