allocPool: Add tests and fix numerous bugs.

This commit is contained in:
Timothée Leclaire-Fournier 2024-03-01 15:19:52 -05:00
parent 0af20178b8
commit dead668fb7
7 changed files with 139 additions and 84 deletions

View File

@ -4,5 +4,6 @@ project(allocPool)
set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD 23)
add_executable(allocPool main.cpp add_executable(allocPool main.cpp
allocPool.cpp allocPool.hpp
allocPool.hpp) tests.cpp
tests.hpp)

View File

@ -1,8 +1,8 @@
# allocPool - A simple & high performance object pool using modern C++ # allocPool - A simple & high performance object pool using modern C++
This is allocPool, a pool of objects that allow you to avoid expensive allocations This is allocPool, a pool of objects in a single header file that allow you to
during runtime. This preallocates objects in the constructor (with threads) then avoid expensive allocations during runtime. This preallocates objects in the
offers you two functions: getPtr() and returnPtr(ptr). constructor (with threads) then offers you two functions: `getPtr()` and `returnPtr(ptr)`.
Using C++ concepts, we can use templates and require the class given to have a Using C++ concepts, we can use templates and require the class given to have a
default constructor and to have a .reset() function. It will be used to clean the default constructor and to have a .reset() function. It will be used to clean the
@ -10,4 +10,5 @@ objects before giving them to another caller.
This pool uses a hashmap and a pivot to make returnPtr(ptr) extremely fast. This pool uses a hashmap and a pivot to make returnPtr(ptr) extremely fast.
It will automatically grow when the max capacity is reached. It will automatically grow when the max capacity is reached, though there will
be a performance penalty.

View File

@ -1,71 +0,0 @@
#include "allocPool.hpp"
template<class T>
requires std::default_initializable<T> && resetable<T>
allocPool<T>::allocPool(size_t defaultAllocNumbers)
: vec(defaultAllocNumbers), pivot{defaultAllocNumbers} {
memset(&(vec[0]), 0, sizeof(vec[0]) * vec.size());
initArray(defaultAllocNumbers);
}
template<class T>
requires std::default_initializable<T> && resetable<T>
T *allocPool<T>::getPtr() {
if (pivot == 0)
resizeVec();
auto *ptrToReturn{vec[0]};
std::swap(vec[0], vec[pivot - 1]);
positionMap[vec[0]] = 0;
positionMap[vec[pivot - 1]] = pivot - 1;
pivot--;
return ptrToReturn;
}
template<class T>
requires std::default_initializable<T> && resetable<T>
void allocPool<T>::returnPtr(T *ptr) {
size_t pos = positionMap[ptr];
vec[pos].reset();
std::swap(vec[pos], vec[pivot]);
pivot++;
}
template<class T>
requires std::default_initializable<T> && resetable<T>
void allocPool<T>::initArray(size_t amount) {
const auto amountOfThreads{std::thread::hardware_concurrency()};
assert(amountOfThreads);
const auto amountPerThreads{amount / amountOfThreads};
std::vector<std::thread> threads;
threads.reserve(amountOfThreads);
for (size_t i{}; i < amountOfThreads; i++)
threads.emplace_back(&allocPool::initObjects, this, i, amountPerThreads);
for (auto &t: threads)
t.join();
// Remainder
initObjects(vec[vec.size() - (amount % amountOfThreads)], amount % amountOfThreads);
}
template<class T>
requires std::default_initializable<T> && resetable<T>
void allocPool<T>::initObjects(size_t startIdx, size_t amount) {
for (size_t i{}; i < amount; i++) {
// TODO: Be more cache friendly by making a vector per thread, then doing memcpy into the original vector.
vec[startIdx + i] = new T;
positionMap[startIdx + i] = i;
}
}
template<class T>
requires std::default_initializable<T> && resetable<T>
void allocPool<T>::resizeVec() {
size_t size{vec.size()};
vec.resize(2 * size);
memcpy(&(vec[size]), &(vec[0]), size);
initArray(size);
}

View File

@ -18,17 +18,73 @@ template<class T>
requires std::default_initializable<T> && resetable<T> requires std::default_initializable<T> && resetable<T>
class allocPool { class allocPool {
public: public:
explicit allocPool(size_t defaultAllocNumbers = 1000); explicit allocPool(size_t defaultAllocNumbers = 1000)
: vec(defaultAllocNumbers), pivot{defaultAllocNumbers} {
memset(&(vec[0]), 0, sizeof(vec[0]) * vec.size());
initArray(defaultAllocNumbers);
}
T *getPtr(); T *getPtr() {
void returnPtr(T *ptr); if (pivot == 0)
resizeVec();
auto *ptrToReturn{vec[0]};
std::swap(vec[0], vec[pivot - 1]);
positionMap[vec[0]] = 0;
positionMap[vec[pivot - 1]] = pivot - 1;
pivot--;
return ptrToReturn;
}
void returnPtr(T *ptr) {
size_t pos = positionMap[ptr];
(vec[pos])->reset();
std::swap(vec[pos], vec[pivot]);
positionMap[vec[pos]] = pos;
positionMap[vec[pivot]] = pivot;
pivot++;
}
private: private:
std::vector<T *> vec; std::vector<T *> vec;
std::unordered_map<T *, size_t> positionMap; std::unordered_map<T *, size_t> positionMap;
size_t pivot; size_t pivot;
void initArray(size_t amount); void initArray(size_t amount) {
void initObjects(size_t startIdx, size_t amount); const auto amountOfThreads{std::thread::hardware_concurrency()};
void resizeVec(); assert(amountOfThreads);
const auto amountPerThreads{amount / amountOfThreads};
std::vector<std::thread> threads;
threads.reserve(amountOfThreads);
for (size_t i{}; i < amountOfThreads; i++)
threads.emplace_back(&allocPool::initObjects, this, i, amountPerThreads);
for (auto &t: threads)
t.join();
// Remainder
initObjects(amount - (amount % amountOfThreads), amount % amountOfThreads);
}
void initObjects(size_t startIdx, size_t amount) {
for (size_t i{}; i < amount; i++) {
// TODO: Be more cache friendly by making a vector per thread, then doing memcpy into the original vector.
vec[startIdx + i] = new T;
positionMap[vec[startIdx + i]] = i;
}
}
void resizeVec() {
size_t size{vec.size()};
vec.resize(2 * size);
pivot = size;
memcpy(&(vec[size]), &(vec[0]), sizeof(vec[0]) * size);
for (size_t i{}; i < size; i++)
positionMap[vec[size + i]] = size + i;
initArray(size);
}
}; };

View File

@ -1,5 +1,7 @@
#include <iostream> #include "tests.hpp"
int main() { int main() {
tests t;
t.runTests();
return 0; return 0;
} }

27
tests.cpp Normal file
View File

@ -0,0 +1,27 @@
#include "tests.hpp"
// Variable statique de la classe tests. Elle doit être
// définie dans le fichier .cpp pour résoudre un problème
// de linker.
std::vector<void (*)()> tests::vec;
tests::tests() {
// Ajuster en fonction du nombre de tests.
vec.reserve(10);
}
void tests::runTests() {
for (auto &i: vec) i();
}
ADD_TEST(allocPoolSimple) {
allocPool<stub> pool(2);
auto *var1{pool.getPtr()};
auto *var2{pool.getPtr()};
auto *var3{pool.getPtr()};
pool.returnPtr(var2);
auto *var4{pool.getPtr()};
pool.returnPtr(var1);
pool.returnPtr(var4);
pool.returnPtr(var3);
}

39
tests.hpp Normal file
View File

@ -0,0 +1,39 @@
#pragma once
#include <cassert>
#include <vector>
#include "allocPool.hpp"
// On ajoute un pointeur de la fonction au vecteur vec. Cela permet de
// tout exécuter de façon propre.
//
// On utilise une struct avec un constructeur qui se fait appeler par
// défaut à sa construction. Lors de celle-ci, on met le pointeur dans
// le vecteur.
//
// Pour ajouter un test, simplement faire une déclaration de fonction
// avec ce macro dans le fichier Tests.cpp
#define ADD_TEST(name) \
void name(); \
struct name##_adder { \
name##_adder() { \
tests::vec.push_back(&name); \
} \
} name##_instance; \
void name()
class tests {
public:
tests();
void runTests();
static std::vector<void (*)()> vec;
};
class stub {
public:
stub() = default;
void reset() {}
private:
int i = 15;
};