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)
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++
This is allocPool, a pool of objects that allow you to avoid expensive allocations
during runtime. This preallocates objects in the constructor (with threads) then
offers you two functions: getPtr() and returnPtr(ptr).
This is allocPool, a pool of objects in a single header file that allow you to
avoid expensive allocations during runtime. This preallocates objects in the
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
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.
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>
class allocPool {
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();
void returnPtr(T *ptr);
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;
}
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:
std::vector<T *> vec;
std::unordered_map<T *, size_t> positionMap;
size_t pivot;
void initArray(size_t amount);
void initObjects(size_t startIdx, size_t amount);
void resizeVec();
void 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(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() {
tests t;
t.runTests();
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;
};