mirror of
https://github.com/bshoshany/thread-pool.git
synced 2026-07-21 19:13:00 +04:00
1113 lines
40 KiB
C++
1113 lines
40 KiB
C++
// Get rid of annoying MSVC warning.
|
|
#ifdef _MSC_VER
|
|
#define _CRT_SECURE_NO_WARNINGS
|
|
#endif
|
|
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <random>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "thread_pool.hpp"
|
|
|
|
// Define short names for commonly-used integer types.
|
|
typedef std::int_fast32_t i32;
|
|
typedef std::uint_fast32_t ui32;
|
|
typedef std::int_fast64_t i64;
|
|
typedef std::uint_fast64_t ui64;
|
|
|
|
// Define two global synced_streams objects: one prints to std::cout and the other to a file.
|
|
synced_stream sync_cout(std::cout);
|
|
std::ofstream log_file;
|
|
synced_stream sync_file(log_file);
|
|
|
|
// A global thread pool object.
|
|
thread_pool pool;
|
|
|
|
// A global random_device object used to seed some random number generators.
|
|
std::random_device rd;
|
|
|
|
// Global variables to measure how many checks succeeded and how many failed.
|
|
ui32 tests_succeeded = 0;
|
|
ui32 tests_failed = 0;
|
|
|
|
/**
|
|
* @brief Print any number of items into both std::cout and the log file, syncing both independently.
|
|
*
|
|
* @tparam T The types of the items.
|
|
* @param items The items to print.
|
|
*/
|
|
template <typename... T>
|
|
void dual_print(const T &...items)
|
|
{
|
|
sync_cout.print(items...);
|
|
sync_file.print(items...);
|
|
}
|
|
|
|
/**
|
|
* @brief Print any number of items into both std::cout and the log file, followed by a newline character, syncing both independently.
|
|
*
|
|
* @tparam T The types of the items.
|
|
* @param items The items to print.
|
|
*/
|
|
template <typename... T>
|
|
void dual_println(const T &...items)
|
|
{
|
|
dual_print(items..., '\n');
|
|
}
|
|
|
|
/**
|
|
* @brief Print a stylized header.
|
|
*
|
|
* @param text The text of the header. Will appear between two lines.
|
|
* @param symbol The symbol to use for the lines. Default is '='.
|
|
*/
|
|
void print_header(const std::string &text, const char &symbol = '=')
|
|
{
|
|
dual_println();
|
|
dual_println(std::string(text.length(), symbol));
|
|
dual_println(text);
|
|
dual_println(std::string(text.length(), symbol));
|
|
}
|
|
|
|
/**
|
|
* @brief Get a string representing the current time.
|
|
*
|
|
* @return The string.
|
|
*/
|
|
std::string get_time()
|
|
{
|
|
const std::time_t t = std::time(nullptr);
|
|
char time_string[32];
|
|
std::strftime(time_string, sizeof(time_string), "%Y-%m-%d_%H.%M.%S", std::localtime(&t));
|
|
return std::string(time_string);
|
|
}
|
|
|
|
/**
|
|
* @brief Check if a condition is met, report the result, and count the number of successes and failures.
|
|
*
|
|
* @param condition The condition to check.
|
|
*/
|
|
void check(const bool condition)
|
|
{
|
|
if (condition)
|
|
{
|
|
dual_println("-> PASSED!");
|
|
tests_succeeded++;
|
|
}
|
|
else
|
|
{
|
|
dual_println("-> FAILED!");
|
|
tests_failed++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Store the ID of the current thread in memory. Waits for a short time to ensure it does not get evaluated by more than one thread.
|
|
*
|
|
* @param location A pointer to the location where the thread ID should be stored.
|
|
*/
|
|
void store_ID(std::thread::id *location)
|
|
{
|
|
*location = std::this_thread::get_id();
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
}
|
|
|
|
/**
|
|
* @brief Count the number of unique threads in the thread pool to ensure that the correct number of individual threads was created. Pushes a number of tasks equal to four times the thread count into the thread pool, and count the number of unique thread IDs returned by the tasks.
|
|
*/
|
|
ui32 count_unique_threads()
|
|
{
|
|
std::vector<std::thread::id> thread_IDs(pool.get_thread_count() * 4);
|
|
for (std::thread::id &id : thread_IDs)
|
|
pool.push_task(store_ID, &id);
|
|
pool.wait_for_tasks();
|
|
std::sort(thread_IDs.begin(), thread_IDs.end());
|
|
ui32 unique_threads = (ui32)(std::unique(thread_IDs.begin(), thread_IDs.end()) - thread_IDs.begin());
|
|
return unique_threads;
|
|
}
|
|
|
|
/**
|
|
* @brief Check that the constructor works.
|
|
*/
|
|
void check_constructor()
|
|
{
|
|
dual_println("Checking that the thread pool reports a number of threads equal to the hardware concurrency...");
|
|
check(pool.get_thread_count() == std::thread::hardware_concurrency());
|
|
dual_println("Checking that the manually counted number of unique thread IDs is equal to the reported number of threads...");
|
|
check(pool.get_thread_count() == count_unique_threads());
|
|
}
|
|
|
|
/**
|
|
* @brief Check that reset() works.
|
|
*/
|
|
void check_reset()
|
|
{
|
|
pool.reset(std::thread::hardware_concurrency() / 2);
|
|
dual_println("Checking that after reset() the thread pool reports a number of threads equal to half the hardware concurrency...");
|
|
check(pool.get_thread_count() == std::thread::hardware_concurrency() / 2);
|
|
dual_println("Checking that after reset() the manually counted number of unique thread IDs is equal to the reported number of threads...");
|
|
check(pool.get_thread_count() == count_unique_threads());
|
|
pool.reset(std::thread::hardware_concurrency());
|
|
dual_println("Checking that after a second reset() the thread pool reports a number of threads equal to the hardware concurrency...");
|
|
check(pool.get_thread_count() == std::thread::hardware_concurrency());
|
|
dual_println("Checking that after a second reset() the manually counted number of unique thread IDs is equal to the reported number of threads...");
|
|
check(pool.get_thread_count() == count_unique_threads());
|
|
}
|
|
|
|
/**
|
|
* @brief Check that push_task() works.
|
|
*/
|
|
void check_push_task()
|
|
{
|
|
dual_println("Checking that push_task() works for a function with no arguments or return value...");
|
|
{
|
|
bool flag = false;
|
|
pool.push_task([&flag]
|
|
{ flag = true; });
|
|
pool.wait_for_tasks();
|
|
check(flag);
|
|
}
|
|
dual_println("Checking that push_task() works for a function with one argument and no return value...");
|
|
{
|
|
bool flag = false;
|
|
pool.push_task([](bool *flag)
|
|
{ *flag = true; },
|
|
&flag);
|
|
pool.wait_for_tasks();
|
|
check(flag);
|
|
}
|
|
dual_println("Checking that push_task() works for a function with two arguments and no return value...");
|
|
{
|
|
bool flag1 = false;
|
|
bool flag2 = false;
|
|
pool.push_task([](bool *flag1, bool *flag2)
|
|
{ *flag1 = *flag2 = true; },
|
|
&flag1, &flag2);
|
|
pool.wait_for_tasks();
|
|
check(flag1 && flag2);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Check that submit() works.
|
|
*/
|
|
void check_submit()
|
|
{
|
|
dual_println("Checking that submit() works for a function with no arguments or return value...");
|
|
{
|
|
bool flag = false;
|
|
auto my_future = pool.submit([&flag]
|
|
{ flag = true; });
|
|
check(my_future.get() && flag);
|
|
}
|
|
dual_println("Checking that submit() works for a function with one argument and no return value...");
|
|
{
|
|
bool flag = false;
|
|
auto my_future = pool.submit([](bool *flag)
|
|
{ *flag = true; },
|
|
&flag);
|
|
check(my_future.get() && flag);
|
|
}
|
|
dual_println("Checking that submit() works for a function with two arguments and no return value...");
|
|
{
|
|
bool flag1 = false;
|
|
bool flag2 = false;
|
|
auto my_future = pool.submit([](bool *flag1, bool *flag2)
|
|
{ *flag1 = *flag2 = true; },
|
|
&flag1, &flag2);
|
|
check(my_future.get() && flag1 && flag2);
|
|
}
|
|
dual_println("Checking that submit() works for a function with no arguments and a return value...");
|
|
{
|
|
bool flag = false;
|
|
auto my_future = pool.submit([&flag]
|
|
{
|
|
flag = true;
|
|
return 42;
|
|
});
|
|
check(my_future.get() == 42 && flag);
|
|
}
|
|
dual_println("Checking that submit() works for a function with one argument and a return value...");
|
|
{
|
|
bool flag = false;
|
|
auto my_future = pool.submit([](bool *flag)
|
|
{
|
|
*flag = true;
|
|
return 42;
|
|
},
|
|
&flag);
|
|
check(my_future.get() == 42 && flag);
|
|
}
|
|
dual_println("Checking that submit() works for a function with two arguments and a return value...");
|
|
{
|
|
bool flag1 = false;
|
|
bool flag2 = false;
|
|
auto my_future = pool.submit([](bool *flag1, bool *flag2)
|
|
{
|
|
*flag1 = *flag2 = true;
|
|
return 42;
|
|
},
|
|
&flag1, &flag2);
|
|
check(my_future.get() == 42 && flag1 && flag2);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Check that wait_for_tasks() works.
|
|
*/
|
|
void check_wait_for_tasks()
|
|
{
|
|
ui32 n = pool.get_thread_count() * 10;
|
|
std::vector<std::atomic<bool>> flags(n);
|
|
for (ui32 i = 0; i < n; i++)
|
|
pool.push_task([&flags, i]
|
|
{
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
flags[i] = true;
|
|
});
|
|
pool.wait_for_tasks();
|
|
bool all_flags = true;
|
|
for (ui32 i = 0; i < n; i++)
|
|
all_flags = all_flags && flags[i];
|
|
check(all_flags);
|
|
}
|
|
|
|
/**
|
|
* @brief Check that parallelize_loop() works for a specific number of indices split over a specific number of tasks.
|
|
*
|
|
* @param start The first index in the loop.
|
|
* @param end The last index in the loop plus 1.
|
|
* @param num_tasks The number of tasks.
|
|
*/
|
|
void check_parallelize_loop(i32 start, i32 end, const ui32 &num_tasks)
|
|
{
|
|
if (start == end)
|
|
end++;
|
|
dual_println("Verifying that a loop from ", start, " to ", end, " with ", num_tasks, num_tasks == 1 ? " task" : " tasks", " modifies all indices...");
|
|
ui64 num_indices = (ui64)std::abs(end - start);
|
|
i32 offset = std::min(start, end);
|
|
std::vector<std::atomic<bool>> flags((ui64)num_indices);
|
|
pool.parallelize_loop(
|
|
start, end, [&flags, &offset](const i32 &start, const i32 &end)
|
|
{
|
|
for (i32 i = start; i < end; i++)
|
|
flags[(ui64)(i - offset)] = true;
|
|
},
|
|
num_tasks);
|
|
bool all_flags = true;
|
|
for (ui64 i = 0; i < num_indices; i++)
|
|
all_flags = all_flags && flags[i];
|
|
check(all_flags);
|
|
}
|
|
|
|
/**
|
|
* @brief Check that parallelize_loop() works using several different random values for the range of indices and number of tasks.
|
|
*/
|
|
void check_parallelize_loop()
|
|
{
|
|
std::mt19937_64 mt(rd());
|
|
std::uniform_int_distribution<i32> index_dist((i32)pool.get_thread_count() * -100, (i32)pool.get_thread_count() * 100);
|
|
std::uniform_int_distribution<ui32> task_dist(1, pool.get_thread_count());
|
|
for (ui32 i = 0; i < 10; i++)
|
|
check_parallelize_loop(index_dist(mt), index_dist(mt), task_dist(mt));
|
|
}
|
|
|
|
/**
|
|
* @brief Check that sleep_duration works for a specific value.
|
|
*
|
|
* @param duration The value of sleep_duration.
|
|
*/
|
|
void check_sleep_duration(const ui32 &duration)
|
|
{
|
|
dual_println("Submitting tasks with sleep_duration = ", duration, " microseconds...");
|
|
pool.sleep_duration = duration;
|
|
ui32 n = pool.get_thread_count() * 100;
|
|
std::vector<std::atomic<bool>> flags(n);
|
|
for (ui32 i = 0; i < n; i++)
|
|
pool.push_task([&flags, i]
|
|
{ flags[i] = true; });
|
|
pool.wait_for_tasks();
|
|
bool all_flags = true;
|
|
for (ui32 i = 0; i < n; i++)
|
|
all_flags = all_flags && flags[i];
|
|
check(all_flags);
|
|
}
|
|
|
|
/**
|
|
* @brief Check that sleep_duration works for several different random values.
|
|
*/
|
|
void check_sleep_duration()
|
|
{
|
|
ui32 old_duration = pool.sleep_duration;
|
|
check_sleep_duration(0);
|
|
std::mt19937_64 mt(rd());
|
|
std::uniform_int_distribution<ui32> dist(1, 2000);
|
|
for (ui32 i = 0; i < 5; i++)
|
|
check_sleep_duration(dist(mt));
|
|
dual_println("Resetting sleep_duration to the default value (", old_duration, " microseconds).");
|
|
pool.sleep_duration = old_duration;
|
|
}
|
|
|
|
/**
|
|
* @brief Check that task monitoring works.
|
|
*/
|
|
void check_task_monitoring()
|
|
{
|
|
ui32 n = std::min<ui32>(std::thread::hardware_concurrency(), 4);
|
|
dual_println("Resetting pool to ", n, " threads.");
|
|
pool.reset(n);
|
|
dual_println("Submitting ", n * 3, " tasks.");
|
|
std::vector<std::atomic<bool>> release(n * 3);
|
|
for (ui32 i = 0; i < n * 3; i++)
|
|
pool.push_task([&release, i]
|
|
{
|
|
while (!release[i])
|
|
std::this_thread::yield();
|
|
dual_println("Task ", i, " released.");
|
|
});
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
|
dual_println("After submission, should have: ", n * 3, " tasks total, ", n, " tasks running, ", n * 2, " tasks queued...");
|
|
check(pool.get_tasks_total() == n * 3 && pool.get_tasks_running() == n && pool.get_tasks_queued() == n * 2);
|
|
for (ui32 i = 0; i < n; i++)
|
|
release[i] = true;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
|
dual_println("After releasing ", n, " tasks, should have: ", n * 2, " tasks total, ", n, " tasks running, ", n, " tasks queued...");
|
|
for (ui32 i = n; i < n * 2; i++)
|
|
release[i] = true;
|
|
check(pool.get_tasks_total() == n * 2 && pool.get_tasks_running() == n && pool.get_tasks_queued() == n);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
|
dual_println("After releasing ", n, " more tasks, should have: ", n, " tasks total, ", n, " tasks running, ", 0, " tasks queued...");
|
|
check(pool.get_tasks_total() == n && pool.get_tasks_running() == n && pool.get_tasks_queued() == 0);
|
|
for (ui32 i = n * 2; i < n * 3; i++)
|
|
release[i] = true;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
dual_println("After releasing the final ", n, " tasks, should have: ", 0, " tasks total, ", 0, " tasks running, ", 0, " tasks queued...");
|
|
check(pool.get_tasks_total() == 0 && pool.get_tasks_running() == 0 && pool.get_tasks_queued() == 0);
|
|
dual_println("Resetting pool to ", std::thread::hardware_concurrency(), " threads.");
|
|
pool.reset(std::thread::hardware_concurrency());
|
|
}
|
|
|
|
/**
|
|
* @brief Check that pausing works.
|
|
*/
|
|
void check_pausing()
|
|
{
|
|
ui32 n = std::min<ui32>(std::thread::hardware_concurrency(), 4);
|
|
dual_println("Resetting pool to ", n, " threads.");
|
|
pool.reset(n);
|
|
dual_println("Pausing pool.");
|
|
pool.paused = true;
|
|
dual_println("Submitting ", n * 3, " tasks, each one waiting for 200ms.");
|
|
for (ui32 i = 0; i < n * 3; i++)
|
|
pool.push_task([i]
|
|
{
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
dual_println("Task ", i, " done.");
|
|
});
|
|
dual_println("Immediately after submission, should have: ", n * 3, " tasks total, ", 0, " tasks running, ", n * 3, " tasks queued...");
|
|
check(pool.get_tasks_total() == n * 3 && pool.get_tasks_running() == 0 && pool.get_tasks_queued() == n * 3);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
|
dual_println("300ms later, should still have: ", n * 3, " tasks total, ", 0, " tasks running, ", n * 3, " tasks queued...");
|
|
check(pool.get_tasks_total() == n * 3 && pool.get_tasks_running() == 0 && pool.get_tasks_queued() == n * 3);
|
|
dual_println("Unpausing pool.");
|
|
pool.paused = false;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
|
dual_println("300ms later, should have: ", n * 2, " tasks total, ", n, " tasks running, ", n, " tasks queued...");
|
|
check(pool.get_tasks_total() == n * 2 && pool.get_tasks_running() == n && pool.get_tasks_queued() == n);
|
|
dual_println("Pausing pool and using wait_for_tasks() to wait for the running tasks.");
|
|
pool.paused = true;
|
|
pool.wait_for_tasks();
|
|
dual_println("After waiting, should have: ", n, " tasks total, ", 0, " tasks running, ", n, " tasks queued...");
|
|
check(pool.get_tasks_total() == n && pool.get_tasks_running() == 0 && pool.get_tasks_queued() == n);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
dual_println("200ms later, should still have: ", n, " tasks total, ", 0, " tasks running, ", n, " tasks queued...");
|
|
check(pool.get_tasks_total() == n && pool.get_tasks_running() == 0 && pool.get_tasks_queued() == n);
|
|
dual_println("Unpausing pool and using wait_for_tasks() to wait for all tasks.");
|
|
pool.paused = false;
|
|
pool.wait_for_tasks();
|
|
dual_println("After waiting, should have: ", 0, " tasks total, ", 0, " tasks running, ", 0, " tasks queued...");
|
|
check(pool.get_tasks_total() == 0 && pool.get_tasks_running() == 0 && pool.get_tasks_queued() == 0);
|
|
dual_println("Resetting pool to ", std::thread::hardware_concurrency(), " threads.");
|
|
pool.reset(std::thread::hardware_concurrency());
|
|
}
|
|
|
|
/**
|
|
* @brief Check that exception handling work.
|
|
*/
|
|
void check_exceptions()
|
|
{
|
|
bool caught = false;
|
|
auto my_future = pool.submit([]
|
|
{ throw std::runtime_error("Exception thrown!"); });
|
|
try
|
|
{
|
|
my_future.get();
|
|
}
|
|
catch (const std::exception &e)
|
|
{
|
|
if (e.what() == std::string("Exception thrown!"))
|
|
caught = true;
|
|
}
|
|
check(caught);
|
|
}
|
|
|
|
/**
|
|
* @brief A lightweight matrix class template for performance testing purposes. Not for general use; only contains the bare minimum functionality needed for the test. Based on https://github.com/bshoshany/multithreaded-matrix
|
|
*
|
|
* @tparam T The type to use for the matrix elements.
|
|
*/
|
|
template <typename T>
|
|
class matrix
|
|
{
|
|
public:
|
|
// =====================================
|
|
// Constructors and assignment operators
|
|
// =====================================
|
|
|
|
/**
|
|
* @brief Construct an uninitialized matrix.
|
|
*
|
|
* @param _rows The number of rows.
|
|
* @param _cols The number of columns.
|
|
*/
|
|
matrix(const ui64 &_rows, const ui64 &_cols)
|
|
: rows(_rows), cols(_cols), smart_elements(new T[rows * cols])
|
|
{
|
|
elements = smart_elements.get();
|
|
}
|
|
|
|
/**
|
|
* @brief Construct a new matrix by copying the elements of an existing matrix.
|
|
*
|
|
* @param m The matrix to be copied.
|
|
*/
|
|
matrix(const matrix<T> &m)
|
|
: rows(m.rows), cols(m.cols), smart_elements(new T[rows * cols])
|
|
{
|
|
elements = smart_elements.get();
|
|
for (ui64 i = 0; i < rows * cols; i++)
|
|
elements[i] = m.elements[i];
|
|
}
|
|
|
|
/**
|
|
* @brief Construct a new matrix by moving the elements of an existing matrix.
|
|
*
|
|
* @param m The matrix to be moved.
|
|
*/
|
|
matrix(matrix<T> &&m)
|
|
: rows(m.rows), cols(m.cols), smart_elements(std::move(m.smart_elements))
|
|
{
|
|
elements = smart_elements.get();
|
|
m.rows = 0;
|
|
m.cols = 0;
|
|
m.elements = nullptr;
|
|
}
|
|
|
|
/**
|
|
* @brief Copy the elements of another matrix to this matrix.
|
|
*
|
|
* @param m The matrix to be copied.
|
|
* @return A reference to this matrix.
|
|
*/
|
|
matrix<T> &operator=(const matrix<T> &m)
|
|
{
|
|
rows = m.rows;
|
|
cols = m.cols;
|
|
smart_elements.reset(new T[rows * cols]);
|
|
elements = smart_elements.get();
|
|
for (ui64 i = 0; i < rows * cols; i++)
|
|
elements[i] = m.elements[i];
|
|
return *this;
|
|
}
|
|
|
|
/**
|
|
* @brief Move the elements of another matrix to this matrix.
|
|
*
|
|
* @param m The matrix to be moved.
|
|
* @return A reference to this matrix.
|
|
*/
|
|
matrix<T> &operator=(matrix<T> &&m)
|
|
{
|
|
rows = m.rows;
|
|
cols = m.cols;
|
|
smart_elements = std::move(m.smart_elements);
|
|
elements = smart_elements.get();
|
|
m.rows = 0;
|
|
m.cols = 0;
|
|
m.elements = nullptr;
|
|
return *this;
|
|
}
|
|
|
|
// ====================
|
|
// Overloaded operators
|
|
// ====================
|
|
|
|
/**
|
|
* @brief Read or modify a matrix element.
|
|
*
|
|
* @param row The row index (starting from zero).
|
|
* @param col The column index (starting from zero).
|
|
* @return A reference to the element.
|
|
*/
|
|
inline T &operator()(const ui64 &row, const ui64 &col)
|
|
{
|
|
return elements[(cols * row) + col];
|
|
}
|
|
|
|
/**
|
|
* @brief Read a matrix element.
|
|
*
|
|
* @param row The row index (starting from zero).
|
|
* @param col The column index (starting from zero).
|
|
* @return The value of the element.
|
|
*/
|
|
inline T operator()(const ui64 &row, const ui64 &col) const
|
|
{
|
|
return elements[(cols * row) + col];
|
|
}
|
|
|
|
/**
|
|
* @brief Read or modify an element of the underlying 1-dimensional array.
|
|
*
|
|
* @param i The element index (starting from zero).
|
|
* @return A reference to the element.
|
|
*/
|
|
inline T &operator[](const ui64 &i)
|
|
{
|
|
return elements[i];
|
|
}
|
|
|
|
/**
|
|
* @brief Read an element of the underlying 1-dimensional array.
|
|
*
|
|
* @param i The element index (starting from zero).
|
|
* @return The value of the element.
|
|
*/
|
|
inline T operator[](const ui64 &i) const
|
|
{
|
|
return elements[i];
|
|
}
|
|
|
|
/**
|
|
* @brief Compare this matrix to another matrix.
|
|
*
|
|
* @param m The matrix to compare to.
|
|
* @return Whether the matrices have the same elements.
|
|
*/
|
|
bool operator==(const matrix<T> &m) const
|
|
{
|
|
bool compare_result = true;
|
|
for (ui64 i = 0; i < rows * cols; i++)
|
|
compare_result = compare_result && (elements[i] == m.elements[i]);
|
|
return compare_result;
|
|
}
|
|
|
|
// =======================
|
|
// Public member functions
|
|
// =======================
|
|
|
|
/**
|
|
* @brief Transpose a matrix.
|
|
*
|
|
* @param num_tasks The number of parallel tasks to use. If set to 0, no multithreading will be used.
|
|
* @return The transposed matrix.
|
|
*/
|
|
matrix<T> transpose(const ui32 &num_tasks) const
|
|
{
|
|
matrix<T> out(cols, rows);
|
|
if (num_tasks == 0)
|
|
{
|
|
for (ui64 i = 0; i < out.rows; i++)
|
|
for (ui64 j = 0; j < out.cols; j++)
|
|
out(i, j) = operator()(j, i);
|
|
}
|
|
else
|
|
{
|
|
pool.parallelize_loop(
|
|
0, out.rows, [this, &out](const ui64 &start, const ui64 &end)
|
|
{
|
|
for (ui64 i = start; i < end; i++)
|
|
for (ui64 j = 0; j < out.cols; j++)
|
|
out(i, j) = operator()(j, i);
|
|
},
|
|
num_tasks);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ================
|
|
// Friend functions
|
|
// ================
|
|
|
|
/**
|
|
* @brief Add two matrices using the specified number of parallel tasks.
|
|
*
|
|
* @param a The first matrix to be added.
|
|
* @param b The second matrix to be added.
|
|
* @param num_tasks The number of parallel tasks to use. If set to 0, no multithreading will be used.
|
|
* @return The sum of the matrices.
|
|
*/
|
|
friend matrix<T> add_matrices(const matrix<T> &a, const matrix<T> &b, const ui32 &num_tasks)
|
|
{
|
|
matrix<T> c(a.rows, a.cols);
|
|
if (num_tasks == 0)
|
|
for (ui64 i = 0; i < a.rows * a.cols; i++)
|
|
c[i] = a[i] + b[i];
|
|
else
|
|
pool.parallelize_loop(
|
|
0, a.rows * a.cols, [&a, &b, &c](const ui64 &start, const ui64 &end)
|
|
{
|
|
for (ui64 i = start; i < end; i++)
|
|
c[i] = a[i] + b[i];
|
|
},
|
|
num_tasks);
|
|
return c;
|
|
}
|
|
|
|
/**
|
|
* @brief Multiply two matrices using the specified number of parallel tasks.
|
|
*
|
|
* @param a The first matrix to be multiplied.
|
|
* @param b The second matrix to be multiplied.
|
|
* @param num_tasks The number of parallel tasks to use. If set to 0, no multithreading will be used.
|
|
* @return The product of the matrices.
|
|
*/
|
|
friend matrix<T> multiply_matrices(const matrix<T> &a, const matrix<T> &b, const ui32 &num_tasks)
|
|
{
|
|
matrix<T> c(a.rows, b.cols);
|
|
if (num_tasks == 0)
|
|
{
|
|
for (ui64 i = 0; i < a.rows; i++)
|
|
for (ui64 j = 0; j < b.cols; j++)
|
|
{
|
|
c(i, j) = 0;
|
|
for (ui64 k = 0; k < a.cols; k++)
|
|
c(i, j) += a(i, k) * b(k, j);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pool.parallelize_loop(
|
|
0, a.rows, [&a, &b, &c, &a_cols = a.cols, &b_cols = b.cols](const ui64 &start, const ui64 &end)
|
|
{
|
|
for (ui64 i = start; i < end; i++)
|
|
for (ui64 j = 0; j < b_cols; j++)
|
|
{
|
|
c(i, j) = 0;
|
|
for (ui64 k = 0; k < a_cols; k++)
|
|
c(i, j) += a(i, k) * b(k, j);
|
|
}
|
|
},
|
|
num_tasks);
|
|
}
|
|
return c;
|
|
}
|
|
|
|
private:
|
|
// ========================
|
|
// Private member variables
|
|
// ========================
|
|
|
|
/**
|
|
* @brief The number of rows.
|
|
*/
|
|
ui64 rows = 0;
|
|
|
|
/**
|
|
* @brief The number of columns.
|
|
*/
|
|
ui64 cols = 0;
|
|
|
|
/**
|
|
* @brief A pointer to an array storing the elements of the matrix in flattened 1-dimensional form.
|
|
*/
|
|
T *elements = nullptr;
|
|
|
|
/**
|
|
* @brief A smart pointer to manage the memory allocated for the matrix elements.
|
|
*/
|
|
std::unique_ptr<T[]> smart_elements;
|
|
};
|
|
|
|
/**
|
|
* @brief A class template for generating random matrices.
|
|
*
|
|
* @tparam T The type to use for the matrix elements.
|
|
* @tparam D The distribution to use, e.g. std::uniform_real_distribution<double>.
|
|
*/
|
|
template <typename T, typename D>
|
|
class random_matrix_generator
|
|
{
|
|
public:
|
|
// ============
|
|
// Constructors
|
|
// ============
|
|
|
|
/**
|
|
* @brief Construct a new random matrix generator.
|
|
*
|
|
* @tparam P The types of the parameters to pass to the constructor of the distribution.
|
|
* @param params The parameters to pass to the constructor of the distribution. The number of parameters and their types depends on the particular distribution being used.
|
|
*/
|
|
template <typename... P>
|
|
random_matrix_generator(const P &...params) : dist(params...), rd() {}
|
|
|
|
// =======================
|
|
// Public member functions
|
|
// =======================
|
|
|
|
/**
|
|
* @brief Generate a random matrix with the given number of rows and columns.
|
|
*
|
|
* @param rows The desired number of rows in the matrix.
|
|
* @param cols The desired number of columns in the matrix.
|
|
* @param num_tasks The number of parallel tasks to use. If set to 0, no multithreading will be used.
|
|
* @return The random matrix.
|
|
*/
|
|
matrix<T> generate_matrix(const ui64 &rows, const ui64 &cols, const ui32 &num_tasks)
|
|
{
|
|
matrix<T> m(rows, cols);
|
|
if (num_tasks == 0)
|
|
{
|
|
std::mt19937_64 mt(generate_seed());
|
|
for (ui64 i = 0; i < rows * cols; i++)
|
|
m[i] = dist(mt);
|
|
}
|
|
else
|
|
pool.parallelize_loop(
|
|
0, rows * cols, [this, &m](const ui64 &start, const ui64 &end)
|
|
{
|
|
std::mt19937_64 mt(generate_seed());
|
|
for (ui64 i = start; i < end; i++)
|
|
m[i] = dist(mt);
|
|
},
|
|
num_tasks);
|
|
return m;
|
|
}
|
|
|
|
private:
|
|
// ========================
|
|
// Private member functions
|
|
// ========================
|
|
|
|
/**
|
|
* @brief Generate a seed. The std::mt19937_64 in each block will be seeded using this function in order to avoid depleting the entropy of the random_device.
|
|
*
|
|
* @return A random unsigned 64-bit integer.
|
|
*/
|
|
ui64 generate_seed()
|
|
{
|
|
static std::mt19937_64 mt(rd());
|
|
return mt();
|
|
}
|
|
|
|
// ========================
|
|
// Private member variables
|
|
// ========================
|
|
|
|
/**
|
|
* @brief The distribution to use for generating random numbers.
|
|
*/
|
|
D dist;
|
|
|
|
/**
|
|
* @brief The random device to be used for seeding the pseudo-random number generators.
|
|
*/
|
|
std::random_device rd;
|
|
};
|
|
|
|
/**
|
|
* @brief Check the matrix class template by comparing the results of adding, multiplying, and transposing matrices calculated in two ways: single-threaded and multithreaded.
|
|
*/
|
|
void check_matrix()
|
|
{
|
|
// Initialize a random_matrix_generator object to generates matrices with integers uniformly distributed between -1000 and 1000.
|
|
random_matrix_generator<i64, std::uniform_int_distribution<i64>> rnd(-1000, 1000);
|
|
// Define the size of the matrices to use.
|
|
const ui32 thread_count = pool.get_thread_count();
|
|
const ui64 rows = thread_count * 10;
|
|
const ui64 cols = rows;
|
|
const ui64 total_size = rows * cols;
|
|
dual_println("Using matrices of size ", rows, "x", cols, " with a total of ", total_size, " elements.");
|
|
|
|
matrix<i64> A = rnd.generate_matrix(rows, cols, thread_count);
|
|
matrix<i64> B = rnd.generate_matrix(rows, cols, thread_count);
|
|
|
|
dual_println("Adding two matrices (single-threaded)...");
|
|
matrix<i64> ApB_single = add_matrices(A, B, 0);
|
|
dual_println("Adding two matrices (multithreaded)...");
|
|
matrix<i64> ApB_multi = add_matrices(A, B, thread_count);
|
|
dual_println("Comparing the results...");
|
|
check(ApB_single == ApB_multi);
|
|
|
|
dual_println("Transposing a matrix (single-threaded)...");
|
|
matrix<i64> At_single = A.transpose(0);
|
|
dual_println("Transposing a matrix (multithreaded)...");
|
|
matrix<i64> At_multi = A.transpose(thread_count);
|
|
dual_println("Comparing the results...");
|
|
check(At_single == At_multi);
|
|
|
|
dual_println("Multiplying two matrices (single-threaded)...");
|
|
matrix<i64> AxB_single = multiply_matrices(A, B, 0);
|
|
dual_println("Multiplying two matrices (multithreaded)...");
|
|
matrix<i64> AxB_multi = multiply_matrices(A, B, thread_count);
|
|
dual_println("Comparing the results...");
|
|
check(AxB_single == AxB_multi);
|
|
}
|
|
|
|
/**
|
|
* @brief Print the timing of a specific test.
|
|
*
|
|
* @param num_tasks The number of tasks.
|
|
* @param mean_sd std::pair containing the mean as the first member and standard deviation as the second member.
|
|
*/
|
|
void print_timing(const ui32 &num_tasks, const std::pair<double, double> &mean_sd)
|
|
{
|
|
if (num_tasks == 1)
|
|
dual_print("With 1 task");
|
|
else
|
|
dual_print("With ", std::setw(3), num_tasks, " tasks");
|
|
dual_println(", mean execution time was ", std::setw(6), mean_sd.first, " ms with standard deviation ", std::setw(4), mean_sd.second, " ms.");
|
|
}
|
|
|
|
/**
|
|
* @brief Calculate and print the speedup obtained by multithreading.
|
|
*
|
|
* @param timings A vector of the timings corresponding to different numbers of tasks.
|
|
* @return The maximum speedup obtained.
|
|
*/
|
|
double print_speedup(const std::vector<double> &timings)
|
|
{
|
|
const auto [min_time, max_time] = std::minmax_element(std::begin(timings), std::end(timings));
|
|
double max_speedup = *max_time / *min_time;
|
|
dual_println("Maximum speedup obtained: ", max_speedup, "x.");
|
|
return max_speedup;
|
|
}
|
|
|
|
/**
|
|
* @brief Calculate the mean and standard deviation of a set of integers.
|
|
*
|
|
* @param timings The integers.
|
|
* @return std::pair containing the mean as the first member and standard deviation as the second member.
|
|
*/
|
|
std::pair<double, double> analyze(const std::vector<i64> &timings)
|
|
{
|
|
double mean = 0;
|
|
for (size_t i = 0; i < timings.size(); i++)
|
|
mean += (double)timings[i] / (double)timings.size();
|
|
double variance = 0;
|
|
for (size_t i = 0; i < timings.size(); i++)
|
|
variance += ((double)timings[i] - mean) * ((double)timings[i] - mean) / (double)timings.size();
|
|
double sd = std::sqrt(variance);
|
|
return std::pair(mean, sd);
|
|
}
|
|
|
|
/**
|
|
* @brief Perform a performance test using some matrix operations.
|
|
*/
|
|
void check_performance()
|
|
{
|
|
// Set the formatting of floating point numbers.
|
|
dual_print(std::fixed, std::setprecision(1));
|
|
|
|
// Initialize a random_matrix_generator object to generates matrices with real (floating-point) numbers uniformly distributed between -1000 and 1000.
|
|
random_matrix_generator<double, std::uniform_real_distribution<double>> rnd(-1000, 1000);
|
|
|
|
// Initialize a timer object to measure the execution time of various operations.
|
|
timer tmr;
|
|
|
|
// If the CPU has more than 8 threads, we leave 2 threads for the rest of the operating system. Otherwise, performance may suffer.
|
|
const ui32 thread_count = pool.get_thread_count() <= 8 ? pool.get_thread_count() : pool.get_thread_count() - 2;
|
|
dual_println("Using ", thread_count, " out of ", pool.get_thread_count(), " threads.");
|
|
|
|
// Define the size of the matrices to use.
|
|
const ui64 rows = thread_count * 200;
|
|
const ui64 cols = rows;
|
|
|
|
// The number of tasks to try for each operation.
|
|
const ui32 try_tasks[] = {1, thread_count / 4, thread_count / 2, thread_count, thread_count * 2, thread_count * 4};
|
|
|
|
// Generate two random test matrices to be used for benchmarking addition, transposition, and random matrix generation.
|
|
matrix<double> A = rnd.generate_matrix(rows, cols, thread_count);
|
|
matrix<double> B = rnd.generate_matrix(rows, cols, thread_count);
|
|
|
|
// Generate two random test matrices to be used for benchmarking multiplication. Since matrix multiplication is O(n^3), we reduce the size of the test matrices so that this operation completes within a reasonable time.
|
|
constexpr ui64 mult_factor = 8;
|
|
matrix<double> X = rnd.generate_matrix(rows / mult_factor, cols / mult_factor, thread_count);
|
|
matrix<double> Y = rnd.generate_matrix(cols / mult_factor, rows / mult_factor, thread_count);
|
|
|
|
// Determine the optimal sleep duration for this system.
|
|
dual_print("Determining the optimal sleep duration...");
|
|
i64 optimal_ms = 0;
|
|
ui64 optimal_sleep = 0;
|
|
for (ui64 sleep = 0; sleep <= 2000; sleep += 100)
|
|
{
|
|
dual_print(".");
|
|
pool.sleep_duration = (ui32)sleep;
|
|
tmr.start();
|
|
matrix<double> C = add_matrices(A, B, thread_count);
|
|
matrix<double> D = A.transpose(thread_count);
|
|
matrix<double> E = multiply_matrices(X, Y, thread_count);
|
|
matrix<double> F = rnd.generate_matrix(rows, cols, thread_count);
|
|
tmr.stop();
|
|
if (tmr.ms() < optimal_ms || optimal_ms == 0)
|
|
{
|
|
optimal_ms = tmr.ms();
|
|
optimal_sleep = sleep;
|
|
}
|
|
}
|
|
if (optimal_sleep == 0)
|
|
dual_println("\nResult: Using std::this_thread::yield() instead of std::this_thread::sleep_for() is optimal.");
|
|
else
|
|
dual_println("\nResult: The optimal sleep duration is ", optimal_sleep, " microseconds.");
|
|
pool.sleep_duration = (ui32)optimal_sleep;
|
|
|
|
// Vectors to store statistics.
|
|
std::vector<double> different_n_timings;
|
|
std::vector<i64> same_n_timings;
|
|
std::vector<double> speedups;
|
|
|
|
// How many times to run each test.
|
|
constexpr ui32 repeat = 20;
|
|
|
|
dual_println("\nAdding two ", rows, "x", cols, " matrices ", repeat, " times:");
|
|
for (ui32 n : try_tasks)
|
|
{
|
|
for (ui32 i = 0; i < repeat; i++)
|
|
{
|
|
tmr.start();
|
|
matrix<double> C = add_matrices(A, B, n);
|
|
tmr.stop();
|
|
same_n_timings.push_back(tmr.ms());
|
|
}
|
|
auto mean_sd = analyze(same_n_timings);
|
|
print_timing(n, mean_sd);
|
|
different_n_timings.push_back(mean_sd.first);
|
|
same_n_timings.clear();
|
|
}
|
|
speedups.push_back(print_speedup(different_n_timings));
|
|
different_n_timings.clear();
|
|
|
|
dual_println("\nTransposing one ", rows, "x", cols, " matrix ", repeat, " times:");
|
|
for (ui32 n : try_tasks)
|
|
{
|
|
for (ui32 i = 0; i < repeat; i++)
|
|
{
|
|
tmr.start();
|
|
matrix<double> C = A.transpose(n);
|
|
tmr.stop();
|
|
same_n_timings.push_back(tmr.ms());
|
|
}
|
|
auto mean_sd = analyze(same_n_timings);
|
|
print_timing(n, mean_sd);
|
|
different_n_timings.push_back(mean_sd.first);
|
|
same_n_timings.clear();
|
|
}
|
|
speedups.push_back(print_speedup(different_n_timings));
|
|
different_n_timings.clear();
|
|
|
|
dual_println("\nMultiplying two ", rows / mult_factor, "x", cols / mult_factor, " matrices ", repeat, " times:");
|
|
for (ui32 n : try_tasks)
|
|
{
|
|
for (ui32 i = 0; i < repeat; i++)
|
|
{
|
|
tmr.start();
|
|
matrix<double> C = multiply_matrices(X, Y, n);
|
|
tmr.stop();
|
|
same_n_timings.push_back(tmr.ms());
|
|
}
|
|
auto mean_sd = analyze(same_n_timings);
|
|
print_timing(n, mean_sd);
|
|
different_n_timings.push_back(mean_sd.first);
|
|
same_n_timings.clear();
|
|
}
|
|
speedups.push_back(print_speedup(different_n_timings));
|
|
different_n_timings.clear();
|
|
|
|
dual_println("\nGenerating random ", rows, "x", cols, " matrix ", repeat, " times:");
|
|
for (ui32 n : try_tasks)
|
|
{
|
|
for (ui32 i = 0; i < repeat; i++)
|
|
{
|
|
tmr.start();
|
|
matrix<double> C = rnd.generate_matrix(rows, cols, n);
|
|
tmr.stop();
|
|
same_n_timings.push_back(tmr.ms());
|
|
}
|
|
auto mean_sd = analyze(same_n_timings);
|
|
print_timing(n, mean_sd);
|
|
different_n_timings.push_back(mean_sd.first);
|
|
same_n_timings.clear();
|
|
}
|
|
speedups.push_back(print_speedup(different_n_timings));
|
|
|
|
const double max_speedup = *std::max_element(std::begin(speedups), std::end(speedups));
|
|
dual_println("\nOverall, multithreading provided speedups of up to ", max_speedup, "x.");
|
|
}
|
|
|
|
int main()
|
|
{
|
|
std::string log_filename = "thread_pool_test-" + get_time() + ".log";
|
|
log_file.open(log_filename);
|
|
|
|
dual_println("A C++17 Thread Pool for High-Performance Scientific Computing");
|
|
dual_println("(c) 2021 Barak Shoshany (baraksh@gmail.com) (http://baraksh.com)");
|
|
dual_println("GitHub: https://github.com/bshoshany/thread-pool\n");
|
|
|
|
dual_println("Thread pool library version is ", THREAD_POOL_VERSION, ".");
|
|
dual_println("Hardware concurrency is ", std::thread::hardware_concurrency(), ".");
|
|
dual_println("Generating log file: ", log_filename, ".\n");
|
|
|
|
dual_println("Important: Please do not run any other applications, especially multithreaded applications, in parallel with this test!");
|
|
|
|
print_header("Checking that the constructor works:");
|
|
check_constructor();
|
|
|
|
print_header("Checking that reset() works:");
|
|
check_reset();
|
|
|
|
print_header("Checking that push_task() works:");
|
|
check_push_task();
|
|
|
|
print_header("Checking that submit() works:");
|
|
check_submit();
|
|
|
|
print_header("Checking that wait_for_tasks() works...");
|
|
check_wait_for_tasks();
|
|
|
|
print_header("Checking that parallelize_loop() works:");
|
|
check_parallelize_loop();
|
|
|
|
print_header("Checking that different values of sleep_duration work:");
|
|
check_sleep_duration();
|
|
|
|
print_header("Checking that task monitoring works:");
|
|
check_task_monitoring();
|
|
|
|
print_header("Checking that pausing works:");
|
|
check_pausing();
|
|
|
|
print_header("Checking that exception handling works:");
|
|
check_exceptions();
|
|
|
|
print_header("Testing that matrix operations produce the expected results:");
|
|
check_matrix();
|
|
|
|
if (tests_failed == 0)
|
|
{
|
|
print_header("SUCCESS: Passed all " + std::to_string(tests_succeeded) + " checks!", '+');
|
|
print_header("Performing matrix performance test:");
|
|
check_performance();
|
|
print_header("Thread pool performance test completed!", '+');
|
|
}
|
|
else
|
|
{
|
|
print_header("FAILURE: Passed " + std::to_string(tests_succeeded) + " checks, but failed " + std::to_string(tests_failed) + "!", '+');
|
|
dual_println("\nPlease submit a bug report at https://github.com/bshoshany/thread-pool/issues including the exact specifications of your system (OS, CPU, compiler, etc.) and the generated log file.");
|
|
}
|
|
|
|
return 0;
|
|
}
|