Repository files navigation

testscodecov

poolSTL

Light, self-contained, thread pool-based implementation of C++17 parallel standard library algorithms.

C++17 introduced parallel overloads of standard library algorithms that accept an Execution Policy as the first argument. Policies specify limits on how the implementation may parallelize the algorithm, enabling methods like threads, vectorization, or even GPU. Policies can be supplied by the compiler or by libraries like this one.

std::sort(std::execution::par, vec.begin(), vec.end());
// ^^^^^^^^^^^^^^^^^^^ native C++17 parallel Execution Policy 

Unfortunately compiler support varies. Quick summary of compilers' default standard libraries:

LinuxmacOSWindows
GCC 9+TBB RequiredTBB RequiredTBB Required
GCC 8-
Clang (libc++)
Clang (libstdc++)TBB RequiredTBB RequiredTBB Required
Apple Clang
MSVC 15.7+ (2017)
Parallel STLTBB RequiredTBB RequiredTBB Required
poolSTL✅*✅*✅*

PoolSTL is a supplement to fill in the support gaps. It is not a full implementation; only the basics are covered. However, it is small, easy to integrate, and has no external dependencies. A good backup to the other options.

Use poolSTL exclusively, or only on platforms lacking native support, or only if TBB is not present.

Supports C++11 and higher. Algorithms introduced in C++17 require C++17 or higher.
Tested in CI on GCC 7+, Clang/LLVM 5+, Apple Clang, MSVC, MinGW, and Emscripten.

Implemented Algorithms

Algorithms are added on an as-needed basis. If you need one open an issue or contribute a PR.
Limitations: All iterators must be random access. No nested parallel calls.

<algorithm>

<numeric>

All in std:: namespace.

Other

  • poolstl::iota_iter - Iterate over integers. Same as iterating over output of std::iota but without materializing anything. Iterator version of std::ranges::iota_view.
  • poolstl::for_each_chunk - Like std::for_each, but explicitly splits the input range into chunks then exposes the chunked parallelism. A user-specified chunk constructor is called for each parallel chunk then its output is passed to each loop iteration. Useful for workloads that need an expensive workspace that can be reused between iterations, but not simultaneously by all iterations in parallel.
  • poolstl::pluggable_sort - Like std::sort, but allows specification of sequential sort method. To parallelize pdqsort: pluggable_sort(par, v.begin(), v.end(), pdqsort).

Usage

PoolSTL provides:

  • poolstl::par: Substitute for std::execution::par. Parallelized using a thread pool.
  • poolstl::seq: Substitute for std::execution::seq. Simply calls the regular (non-policy) overload.
  • poolstl::par_if(): Choose parallel or sequential at runtime. See below.

In short, use poolstl::par to make your code parallel. Complete example:

#include<iostream>
#include<poolstl/poolstl.hpp>intmain() {
std::vector<int> v = {0, 1, 2, 3, 4, 5};
auto sum = std::reduce(poolstl::par, vec.cbegin(), vec.cend());
// ^^^^^^^^^^^^// Add this to make your code parallel.
std::cout << "Sum=" << sum << std::endl;
return0;
}

Controlling Thread Pool Size with par.on(pool)

The thread pool used by poolstl::par is managed internally by poolSTL. It is started on first use.
Use your own thread pool with poolstl::par.on(pool) for control over thread count, startup/shutdown, etc.:

task_thread_pool::task_thread_pool pool{4}; // 4 threadsstd::reduce(poolstl::par.on(pool), vec.begin(), vec.end());

Choosing Parallel or Sequential at Runtime with par_if

Sometimes the choice whether to parallelize or not should be made at runtime. For example, small datasets may not amortize the cost of starting threads, while large datasets do and should be parallelized.

Use poolstl::par_if to select between par and seq at runtime:

bool is_parallel = vec.size() > 10000;
std::reduce(poolstl::par_if(is_parallel), vec.begin(), vec.end());

Use poolstl::par_if(is_parallel, pool) to control the thread pool used by par, if selected.

Examples

Parallel for (auto& value : vec)

std::vector<int> vec = {0, 1, 2, 3, 4, 5};
// Parallel for-eachstd::for_each(poolstl::par, vec.begin(), vec.end(), [](auto& value) {
std::cout << value; // loop body
});

Parallel for (int i = 0; i < 100; ++i)

using poolstl::iota_iter;
// parallel for loopstd::for_each(poolstl::par, iota_iter<int>(0), iota_iter<int>(100), [](auto i) {
std::cout << i; // loop body
});

Parallel Sort

std::vector<int> vec = {5, 2, 1, 3, 0, 4};
std::sort(poolstl::par, vec.begin(), vec.end());

Installation

Single File

Each release publishes a single-file amalgamated poolstl.hpp. Simply copy this into your project.

Build requirements:

  • Clang and GCC 8 or older: require -lpthread to use C++11 threads.
  • Emscripten: compile and link with -pthread to use C++11 threads. See docs.

CMake

include(FetchContent)
FetchContent_Declare(
poolSTL
GIT_REPOSITORY https://github.com/alugowski/poolSTL
GIT_TAG main
GIT_SHALLOWTRUE
)
FetchContent_MakeAvailable(poolSTL)
target_link_libraries(YOUR_TARGETpoolSTL::poolSTL)

Alternatively copy or checkout the repo into your project and:

add_subdirectory(poolSTL)

Benchmark

See benchmark/ to compare poolSTL against the standard sequential implementation, and (if available) the native std::execution::par implementation.

Results on an M1 Pro (6 power, 2 efficiency cores), with GCC 13:

-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
all_of()/real_time 19.9 ms 19.9 ms 35
all_of(poolstl::par)/real_time 3.47 ms 0.119 ms 198
all_of(std::execution::par)/real_time 3.45 ms 3.25 ms 213
find_if()/needle_percentile:5/real_time 0.988 ms 0.987 ms 712
find_if()/needle_percentile:50/real_time 9.87 ms 9.86 ms 71
find_if()/needle_percentile:100/real_time 19.7 ms 19.7 ms 36
find_if(poolstl::par)/needle_percentile:5/real_time 0.405 ms 0.050 ms 1730
find_if(poolstl::par)/needle_percentile:50/real_time 1.85 ms 0.096 ms 393
find_if(poolstl::par)/needle_percentile:100/real_time 3.64 ms 0.102 ms 193
find_if(std::execution::par)/needle_percentile:5/real_time 0.230 ms 0.220 ms 3103
find_if(std::execution::par)/needle_percentile:50/real_time 1.75 ms 1.60 ms 410
find_if(std::execution::par)/needle_percentile:100/real_time 3.51 ms 3.24 ms 204
for_each()/real_time 94.6 ms 94.6 ms 7
for_each(poolstl::par)/real_time 18.7 ms 0.044 ms 36
for_each(std::execution::par)/real_time 15.3 ms 12.9 ms 46
sort()/real_time 603 ms 602 ms 1
sort(poolstl::par)/real_time 112 ms 6.64 ms 6
sort(std::execution::par)/real_time 113 ms 102 ms 6
pluggable_sort(poolstl::par, ..., pdqsort)/real_time 71.7 ms 6.67 ms 10
transform()/real_time 95.0 ms 94.9 ms 7
transform(poolstl::par)/real_time 17.4 ms 0.037 ms 38
transform(std::execution::par)/real_time 15.3 ms 13.2 ms 45
exclusive_scan()/real_time 33.7 ms 33.7 ms 21
exclusive_scan(poolstl::par)/real_time 11.6 ms 0.095 ms 55
exclusive_scan(std::execution::par)/real_time 19.8 ms 15.3 ms 32
reduce()/real_time 15.2 ms 15.2 ms 46
reduce(poolstl::par)/real_time 4.06 ms 0.044 ms 169
reduce(std::execution::par)/real_time 3.38 ms 3.16 ms 214

poolSTL as std::execution::par

USE AT YOUR OWN RISK! THIS IS A HACK!

Two-line hack for missing compiler support. A no-op on compilers with support.

If POOLSTL_STD_SUPPLEMENT is defined then poolSTL will check for native compiler support. If not found then poolSTL will alias its poolstl::par as std::execution::par:

#definePOOLSTL_STD_SUPPLEMENT
#include<poolstl/poolstl.hpp>

Now just use std::execution::par as normal, and poolSTL will fill in as necessary. See supplement_test.cpp.

Example use case: You can link against TBB, so you'll use native support on GCC 9+, Clang, MSVC, etc. PoolSTL will fill in automatically on GCC <9 and Apple Clang.

Example use case 2: You'd prefer to use the TBB version, but don't want to fail on systems that don't have it. Simply use the supplement as above, but have your build system (CMake, meson, etc.) check for TBB. If not found, define POOLSTL_STD_SUPPLEMENT_NO_INCLUDE and the supplement will not #include <execution> (and neither should your code!), thus dropping the TBB link requirement. The poolSTL supplement fills in.
See the supplement section of tests/CMakeLists.txt for an example.

About

Light and self-contained implementation of C++17 parallel algorithms.

Topics

Resources

Stars

38 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

testscodecov

poolSTL

Light, self-contained, thread pool-based implementation of C++17 parallel standard library algorithms.

C++17 introduced parallel overloads of standard library algorithms that accept an Execution Policy as the first argument. Policies specify limits on how the implementation may parallelize the algorithm, enabling methods like threads, vectorization, or even GPU. Policies can be supplied by the compiler or by libraries like this one.

std::sort(std::execution::par, vec.begin(), vec.end());
// ^^^^^^^^^^^^^^^^^^^ native C++17 parallel Execution Policy 

Unfortunately compiler support varies. Quick summary of compilers' default standard libraries:

LinuxmacOSWindows
GCC 9+TBB RequiredTBB RequiredTBB Required
GCC 8-
Clang (libc++)
Clang (libstdc++)TBB RequiredTBB RequiredTBB Required
Apple Clang
MSVC 15.7+ (2017)
Parallel STLTBB RequiredTBB RequiredTBB Required
poolSTL✅*✅*✅*

PoolSTL is a supplement to fill in the support gaps. It is not a full implementation; only the basics are covered. However, it is small, easy to integrate, and has no external dependencies. A good backup to the other options.

Use poolSTL exclusively, or only on platforms lacking native support, or only if TBB is not present.

Supports C++11 and higher. Algorithms introduced in C++17 require C++17 or higher.
Tested in CI on GCC 7+, Clang/LLVM 5+, Apple Clang, MSVC, MinGW, and Emscripten.

Implemented Algorithms

Algorithms are added on an as-needed basis. If you need one open an issue or contribute a PR.
Limitations: All iterators must be random access. No nested parallel calls.

<algorithm>

<numeric>

All in std:: namespace.

Other

  • poolstl::iota_iter - Iterate over integers. Same as iterating over output of std::iota but without materializing anything. Iterator version of std::ranges::iota_view.
  • poolstl::for_each_chunk - Like std::for_each, but explicitly splits the input range into chunks then exposes the chunked parallelism. A user-specified chunk constructor is called for each parallel chunk then its output is passed to each loop iteration. Useful for workloads that need an expensive workspace that can be reused between iterations, but not simultaneously by all iterations in parallel.
  • poolstl::pluggable_sort - Like std::sort, but allows specification of sequential sort method. To parallelize pdqsort: pluggable_sort(par, v.begin(), v.end(), pdqsort).

Usage

PoolSTL provides:

  • poolstl::par: Substitute for std::execution::par. Parallelized using a thread pool.
  • poolstl::seq: Substitute for std::execution::seq. Simply calls the regular (non-policy) overload.
  • poolstl::par_if(): Choose parallel or sequential at runtime. See below.

In short, use poolstl::par to make your code parallel. Complete example:

#include<iostream>
#include<poolstl/poolstl.hpp>intmain() {
std::vector<int> v = {0, 1, 2, 3, 4, 5};
auto sum = std::reduce(poolstl::par, vec.cbegin(), vec.cend());
// ^^^^^^^^^^^^// Add this to make your code parallel.
std::cout << "Sum=" << sum << std::endl;
return0;
}

Controlling Thread Pool Size with par.on(pool)

The thread pool used by poolstl::par is managed internally by poolSTL. It is started on first use.
Use your own thread pool with poolstl::par.on(pool) for control over thread count, startup/shutdown, etc.:

task_thread_pool::task_thread_pool pool{4}; // 4 threadsstd::reduce(poolstl::par.on(pool), vec.begin(), vec.end());

Choosing Parallel or Sequential at Runtime with par_if

Sometimes the choice whether to parallelize or not should be made at runtime. For example, small datasets may not amortize the cost of starting threads, while large datasets do and should be parallelized.

Use poolstl::par_if to select between par and seq at runtime:

bool is_parallel = vec.size() > 10000;
std::reduce(poolstl::par_if(is_parallel), vec.begin(), vec.end());

Use poolstl::par_if(is_parallel, pool) to control the thread pool used by par, if selected.

Examples

Parallel for (auto& value : vec)

std::vector<int> vec = {0, 1, 2, 3, 4, 5};
// Parallel for-eachstd::for_each(poolstl::par, vec.begin(), vec.end(), [](auto& value) {
std::cout << value; // loop body
});

Parallel for (int i = 0; i < 100; ++i)

using poolstl::iota_iter;
// parallel for loopstd::for_each(poolstl::par, iota_iter<int>(0), iota_iter<int>(100), [](auto i) {
std::cout << i; // loop body
});

Parallel Sort

std::vector<int> vec = {5, 2, 1, 3, 0, 4};
std::sort(poolstl::par, vec.begin(), vec.end());

Installation

Single File

Each release publishes a single-file amalgamated poolstl.hpp. Simply copy this into your project.

Build requirements:

  • Clang and GCC 8 or older: require -lpthread to use C++11 threads.
  • Emscripten: compile and link with -pthread to use C++11 threads. See docs.

CMake

include(FetchContent)
FetchContent_Declare(
poolSTL
GIT_REPOSITORY https://github.com/alugowski/poolSTL
GIT_TAG main
GIT_SHALLOWTRUE
)
FetchContent_MakeAvailable(poolSTL)
target_link_libraries(YOUR_TARGETpoolSTL::poolSTL)

Alternatively copy or checkout the repo into your project and:

add_subdirectory(poolSTL)

Benchmark

See benchmark/ to compare poolSTL against the standard sequential implementation, and (if available) the native std::execution::par implementation.

Results on an M1 Pro (6 power, 2 efficiency cores), with GCC 13:

-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
all_of()/real_time 19.9 ms 19.9 ms 35
all_of(poolstl::par)/real_time 3.47 ms 0.119 ms 198
all_of(std::execution::par)/real_time 3.45 ms 3.25 ms 213
find_if()/needle_percentile:5/real_time 0.988 ms 0.987 ms 712
find_if()/needle_percentile:50/real_time 9.87 ms 9.86 ms 71
find_if()/needle_percentile:100/real_time 19.7 ms 19.7 ms 36
find_if(poolstl::par)/needle_percentile:5/real_time 0.405 ms 0.050 ms 1730
find_if(poolstl::par)/needle_percentile:50/real_time 1.85 ms 0.096 ms 393
find_if(poolstl::par)/needle_percentile:100/real_time 3.64 ms 0.102 ms 193
find_if(std::execution::par)/needle_percentile:5/real_time 0.230 ms 0.220 ms 3103
find_if(std::execution::par)/needle_percentile:50/real_time 1.75 ms 1.60 ms 410
find_if(std::execution::par)/needle_percentile:100/real_time 3.51 ms 3.24 ms 204
for_each()/real_time 94.6 ms 94.6 ms 7
for_each(poolstl::par)/real_time 18.7 ms 0.044 ms 36
for_each(std::execution::par)/real_time 15.3 ms 12.9 ms 46
sort()/real_time 603 ms 602 ms 1
sort(poolstl::par)/real_time 112 ms 6.64 ms 6
sort(std::execution::par)/real_time 113 ms 102 ms 6
pluggable_sort(poolstl::par, ..., pdqsort)/real_time 71.7 ms 6.67 ms 10
transform()/real_time 95.0 ms 94.9 ms 7
transform(poolstl::par)/real_time 17.4 ms 0.037 ms 38
transform(std::execution::par)/real_time 15.3 ms 13.2 ms 45
exclusive_scan()/real_time 33.7 ms 33.7 ms 21
exclusive_scan(poolstl::par)/real_time 11.6 ms 0.095 ms 55
exclusive_scan(std::execution::par)/real_time 19.8 ms 15.3 ms 32
reduce()/real_time 15.2 ms 15.2 ms 46
reduce(poolstl::par)/real_time 4.06 ms 0.044 ms 169
reduce(std::execution::par)/real_time 3.38 ms 3.16 ms 214

poolSTL as std::execution::par

USE AT YOUR OWN RISK! THIS IS A HACK!

Two-line hack for missing compiler support. A no-op on compilers with support.

If POOLSTL_STD_SUPPLEMENT is defined then poolSTL will check for native compiler support. If not found then poolSTL will alias its poolstl::par as std::execution::par:

#definePOOLSTL_STD_SUPPLEMENT
#include<poolstl/poolstl.hpp>

Now just use std::execution::par as normal, and poolSTL will fill in as necessary. See supplement_test.cpp.

Example use case: You can link against TBB, so you'll use native support on GCC 9+, Clang, MSVC, etc. PoolSTL will fill in automatically on GCC <9 and Apple Clang.

Example use case 2: You'd prefer to use the TBB version, but don't want to fail on systems that don't have it. Simply use the supplement as above, but have your build system (CMake, meson, etc.) check for TBB. If not found, define POOLSTL_STD_SUPPLEMENT_NO_INCLUDE and the supplement will not #include <execution> (and neither should your code!), thus dropping the TBB link requirement. The poolSTL supplement fills in.
See the supplement section of tests/CMakeLists.txt for an example.

About

Light and self-contained implementation of C++17 parallel algorithms.

Topics

Resources

Stars

38 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

testscodecov

poolSTL

Light, self-contained, thread pool-based implementation of C++17 parallel standard library algorithms.

C++17 introduced parallel overloads of standard library algorithms that accept an Execution Policy as the first argument. Policies specify limits on how the implementation may parallelize the algorithm, enabling methods like threads, vectorization, or even GPU. Policies can be supplied by the compiler or by libraries like this one.

std::sort(std::execution::par, vec.begin(), vec.end());
// ^^^^^^^^^^^^^^^^^^^ native C++17 parallel Execution Policy 

Unfortunately compiler support varies. Quick summary of compilers' default standard libraries:

LinuxmacOSWindows
GCC 9+TBB RequiredTBB RequiredTBB Required
GCC 8-
Clang (libc++)
Clang (libstdc++)TBB RequiredTBB RequiredTBB Required
Apple Clang
MSVC 15.7+ (2017)
Parallel STLTBB RequiredTBB RequiredTBB Required
poolSTL✅*✅*✅*

PoolSTL is a supplement to fill in the support gaps. It is not a full implementation; only the basics are covered. However, it is small, easy to integrate, and has no external dependencies. A good backup to the other options.

Use poolSTL exclusively, or only on platforms lacking native support, or only if TBB is not present.

Supports C++11 and higher. Algorithms introduced in C++17 require C++17 or higher.
Tested in CI on GCC 7+, Clang/LLVM 5+, Apple Clang, MSVC, MinGW, and Emscripten.

Implemented Algorithms

Algorithms are added on an as-needed basis. If you need one open an issue or contribute a PR.
Limitations: All iterators must be random access. No nested parallel calls.

<algorithm>

<numeric>

All in std:: namespace.

Other

  • poolstl::iota_iter - Iterate over integers. Same as iterating over output of std::iota but without materializing anything. Iterator version of std::ranges::iota_view.
  • poolstl::for_each_chunk - Like std::for_each, but explicitly splits the input range into chunks then exposes the chunked parallelism. A user-specified chunk constructor is called for each parallel chunk then its output is passed to each loop iteration. Useful for workloads that need an expensive workspace that can be reused between iterations, but not simultaneously by all iterations in parallel.
  • poolstl::pluggable_sort - Like std::sort, but allows specification of sequential sort method. To parallelize pdqsort: pluggable_sort(par, v.begin(), v.end(), pdqsort).

Usage

PoolSTL provides:

  • poolstl::par: Substitute for std::execution::par. Parallelized using a thread pool.
  • poolstl::seq: Substitute for std::execution::seq. Simply calls the regular (non-policy) overload.
  • poolstl::par_if(): Choose parallel or sequential at runtime. See below.

In short, use poolstl::par to make your code parallel. Complete example:

#include<iostream>
#include<poolstl/poolstl.hpp>intmain() {
std::vector<int> v = {0, 1, 2, 3, 4, 5};
auto sum = std::reduce(poolstl::par, vec.cbegin(), vec.cend());
// ^^^^^^^^^^^^// Add this to make your code parallel.
std::cout << "Sum=" << sum << std::endl;
return0;
}

Controlling Thread Pool Size with par.on(pool)

The thread pool used by poolstl::par is managed internally by poolSTL. It is started on first use.
Use your own thread pool with poolstl::par.on(pool) for control over thread count, startup/shutdown, etc.:

task_thread_pool::task_thread_pool pool{4}; // 4 threadsstd::reduce(poolstl::par.on(pool), vec.begin(), vec.end());

Choosing Parallel or Sequential at Runtime with par_if

Sometimes the choice whether to parallelize or not should be made at runtime. For example, small datasets may not amortize the cost of starting threads, while large datasets do and should be parallelized.

Use poolstl::par_if to select between par and seq at runtime:

bool is_parallel = vec.size() > 10000;
std::reduce(poolstl::par_if(is_parallel), vec.begin(), vec.end());

Use poolstl::par_if(is_parallel, pool) to control the thread pool used by par, if selected.

Examples

Parallel for (auto& value : vec)

std::vector<int> vec = {0, 1, 2, 3, 4, 5};
// Parallel for-eachstd::for_each(poolstl::par, vec.begin(), vec.end(), [](auto& value) {
std::cout << value; // loop body
});

Parallel for (int i = 0; i < 100; ++i)

using poolstl::iota_iter;
// parallel for loopstd::for_each(poolstl::par, iota_iter<int>(0), iota_iter<int>(100), [](auto i) {
std::cout << i; // loop body
});

Parallel Sort

std::vector<int> vec = {5, 2, 1, 3, 0, 4};
std::sort(poolstl::par, vec.begin(), vec.end());

Installation

Single File

Each release publishes a single-file amalgamated poolstl.hpp. Simply copy this into your project.

Build requirements:

  • Clang and GCC 8 or older: require -lpthread to use C++11 threads.
  • Emscripten: compile and link with -pthread to use C++11 threads. See docs.

CMake

include(FetchContent)
FetchContent_Declare(
poolSTL
GIT_REPOSITORY https://github.com/alugowski/poolSTL
GIT_TAG main
GIT_SHALLOWTRUE
)
FetchContent_MakeAvailable(poolSTL)
target_link_libraries(YOUR_TARGETpoolSTL::poolSTL)

Alternatively copy or checkout the repo into your project and:

add_subdirectory(poolSTL)

Benchmark

See benchmark/ to compare poolSTL against the standard sequential implementation, and (if available) the native std::execution::par implementation.

Results on an M1 Pro (6 power, 2 efficiency cores), with GCC 13:

-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
all_of()/real_time 19.9 ms 19.9 ms 35
all_of(poolstl::par)/real_time 3.47 ms 0.119 ms 198
all_of(std::execution::par)/real_time 3.45 ms 3.25 ms 213
find_if()/needle_percentile:5/real_time 0.988 ms 0.987 ms 712
find_if()/needle_percentile:50/real_time 9.87 ms 9.86 ms 71
find_if()/needle_percentile:100/real_time 19.7 ms 19.7 ms 36
find_if(poolstl::par)/needle_percentile:5/real_time 0.405 ms 0.050 ms 1730
find_if(poolstl::par)/needle_percentile:50/real_time 1.85 ms 0.096 ms 393
find_if(poolstl::par)/needle_percentile:100/real_time 3.64 ms 0.102 ms 193
find_if(std::execution::par)/needle_percentile:5/real_time 0.230 ms 0.220 ms 3103
find_if(std::execution::par)/needle_percentile:50/real_time 1.75 ms 1.60 ms 410
find_if(std::execution::par)/needle_percentile:100/real_time 3.51 ms 3.24 ms 204
for_each()/real_time 94.6 ms 94.6 ms 7
for_each(poolstl::par)/real_time 18.7 ms 0.044 ms 36
for_each(std::execution::par)/real_time 15.3 ms 12.9 ms 46
sort()/real_time 603 ms 602 ms 1
sort(poolstl::par)/real_time 112 ms 6.64 ms 6
sort(std::execution::par)/real_time 113 ms 102 ms 6
pluggable_sort(poolstl::par, ..., pdqsort)/real_time 71.7 ms 6.67 ms 10
transform()/real_time 95.0 ms 94.9 ms 7
transform(poolstl::par)/real_time 17.4 ms 0.037 ms 38
transform(std::execution::par)/real_time 15.3 ms 13.2 ms 45
exclusive_scan()/real_time 33.7 ms 33.7 ms 21
exclusive_scan(poolstl::par)/real_time 11.6 ms 0.095 ms 55
exclusive_scan(std::execution::par)/real_time 19.8 ms 15.3 ms 32
reduce()/real_time 15.2 ms 15.2 ms 46
reduce(poolstl::par)/real_time 4.06 ms 0.044 ms 169
reduce(std::execution::par)/real_time 3.38 ms 3.16 ms 214

poolSTL as std::execution::par

USE AT YOUR OWN RISK! THIS IS A HACK!

Two-line hack for missing compiler support. A no-op on compilers with support.

If POOLSTL_STD_SUPPLEMENT is defined then poolSTL will check for native compiler support. If not found then poolSTL will alias its poolstl::par as std::execution::par:

#definePOOLSTL_STD_SUPPLEMENT
#include<poolstl/poolstl.hpp>

Now just use std::execution::par as normal, and poolSTL will fill in as necessary. See supplement_test.cpp.

Example use case: You can link against TBB, so you'll use native support on GCC 9+, Clang, MSVC, etc. PoolSTL will fill in automatically on GCC <9 and Apple Clang.

Example use case 2: You'd prefer to use the TBB version, but don't want to fail on systems that don't have it. Simply use the supplement as above, but have your build system (CMake, meson, etc.) check for TBB. If not found, define POOLSTL_STD_SUPPLEMENT_NO_INCLUDE and the supplement will not #include <execution> (and neither should your code!), thus dropping the TBB link requirement. The poolSTL supplement fills in.
See the supplement section of tests/CMakeLists.txt for an example.

About

Light and self-contained implementation of C++17 parallel algorithms.

Topics

Resources

Stars

38 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

testscodecov

poolSTL

Light, self-contained, thread pool-based implementation of C++17 parallel standard library algorithms.

C++17 introduced parallel overloads of standard library algorithms that accept an Execution Policy as the first argument. Policies specify limits on how the implementation may parallelize the algorithm, enabling methods like threads, vectorization, or even GPU. Policies can be supplied by the compiler or by libraries like this one.

std::sort(std::execution::par, vec.begin(), vec.end());
// ^^^^^^^^^^^^^^^^^^^ native C++17 parallel Execution Policy 

Unfortunately compiler support varies. Quick summary of compilers' default standard libraries:

LinuxmacOSWindows
GCC 9+TBB RequiredTBB RequiredTBB Required
GCC 8-
Clang (libc++)
Clang (libstdc++)TBB RequiredTBB RequiredTBB Required
Apple Clang
MSVC 15.7+ (2017)
Parallel STLTBB RequiredTBB RequiredTBB Required
poolSTL✅*✅*✅*

PoolSTL is a supplement to fill in the support gaps. It is not a full implementation; only the basics are covered. However, it is small, easy to integrate, and has no external dependencies. A good backup to the other options.

Use poolSTL exclusively, or only on platforms lacking native support, or only if TBB is not present.

Supports C++11 and higher. Algorithms introduced in C++17 require C++17 or higher.
Tested in CI on GCC 7+, Clang/LLVM 5+, Apple Clang, MSVC, MinGW, and Emscripten.

Implemented Algorithms

Algorithms are added on an as-needed basis. If you need one open an issue or contribute a PR.
Limitations: All iterators must be random access. No nested parallel calls.

<algorithm>

<numeric>

All in std:: namespace.

Other

  • poolstl::iota_iter - Iterate over integers. Same as iterating over output of std::iota but without materializing anything. Iterator version of std::ranges::iota_view.
  • poolstl::for_each_chunk - Like std::for_each, but explicitly splits the input range into chunks then exposes the chunked parallelism. A user-specified chunk constructor is called for each parallel chunk then its output is passed to each loop iteration. Useful for workloads that need an expensive workspace that can be reused between iterations, but not simultaneously by all iterations in parallel.
  • poolstl::pluggable_sort - Like std::sort, but allows specification of sequential sort method. To parallelize pdqsort: pluggable_sort(par, v.begin(), v.end(), pdqsort).

Usage

PoolSTL provides:

  • poolstl::par: Substitute for std::execution::par. Parallelized using a thread pool.
  • poolstl::seq: Substitute for std::execution::seq. Simply calls the regular (non-policy) overload.
  • poolstl::par_if(): Choose parallel or sequential at runtime. See below.

In short, use poolstl::par to make your code parallel. Complete example:

#include<iostream>
#include<poolstl/poolstl.hpp>intmain() {
std::vector<int> v = {0, 1, 2, 3, 4, 5};
auto sum = std::reduce(poolstl::par, vec.cbegin(), vec.cend());
// ^^^^^^^^^^^^// Add this to make your code parallel.
std::cout << "Sum=" << sum << std::endl;
return0;
}

Controlling Thread Pool Size with par.on(pool)

The thread pool used by poolstl::par is managed internally by poolSTL. It is started on first use.
Use your own thread pool with poolstl::par.on(pool) for control over thread count, startup/shutdown, etc.:

task_thread_pool::task_thread_pool pool{4}; // 4 threadsstd::reduce(poolstl::par.on(pool), vec.begin(), vec.end());

Choosing Parallel or Sequential at Runtime with par_if

Sometimes the choice whether to parallelize or not should be made at runtime. For example, small datasets may not amortize the cost of starting threads, while large datasets do and should be parallelized.

Use poolstl::par_if to select between par and seq at runtime:

bool is_parallel = vec.size() > 10000;
std::reduce(poolstl::par_if(is_parallel), vec.begin(), vec.end());

Use poolstl::par_if(is_parallel, pool) to control the thread pool used by par, if selected.

Examples

Parallel for (auto& value : vec)

std::vector<int> vec = {0, 1, 2, 3, 4, 5};
// Parallel for-eachstd::for_each(poolstl::par, vec.begin(), vec.end(), [](auto& value) {
std::cout << value; // loop body
});

Parallel for (int i = 0; i < 100; ++i)

using poolstl::iota_iter;
// parallel for loopstd::for_each(poolstl::par, iota_iter<int>(0), iota_iter<int>(100), [](auto i) {
std::cout << i; // loop body
});

Parallel Sort

std::vector<int> vec = {5, 2, 1, 3, 0, 4};
std::sort(poolstl::par, vec.begin(), vec.end());

Installation

Single File

Each release publishes a single-file amalgamated poolstl.hpp. Simply copy this into your project.

Build requirements:

  • Clang and GCC 8 or older: require -lpthread to use C++11 threads.
  • Emscripten: compile and link with -pthread to use C++11 threads. See docs.

CMake

include(FetchContent)
FetchContent_Declare(
poolSTL
GIT_REPOSITORY https://github.com/alugowski/poolSTL
GIT_TAG main
GIT_SHALLOWTRUE
)
FetchContent_MakeAvailable(poolSTL)
target_link_libraries(YOUR_TARGETpoolSTL::poolSTL)

Alternatively copy or checkout the repo into your project and:

add_subdirectory(poolSTL)

Benchmark

See benchmark/ to compare poolSTL against the standard sequential implementation, and (if available) the native std::execution::par implementation.

Results on an M1 Pro (6 power, 2 efficiency cores), with GCC 13:

-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
all_of()/real_time 19.9 ms 19.9 ms 35
all_of(poolstl::par)/real_time 3.47 ms 0.119 ms 198
all_of(std::execution::par)/real_time 3.45 ms 3.25 ms 213
find_if()/needle_percentile:5/real_time 0.988 ms 0.987 ms 712
find_if()/needle_percentile:50/real_time 9.87 ms 9.86 ms 71
find_if()/needle_percentile:100/real_time 19.7 ms 19.7 ms 36
find_if(poolstl::par)/needle_percentile:5/real_time 0.405 ms 0.050 ms 1730
find_if(poolstl::par)/needle_percentile:50/real_time 1.85 ms 0.096 ms 393
find_if(poolstl::par)/needle_percentile:100/real_time 3.64 ms 0.102 ms 193
find_if(std::execution::par)/needle_percentile:5/real_time 0.230 ms 0.220 ms 3103
find_if(std::execution::par)/needle_percentile:50/real_time 1.75 ms 1.60 ms 410
find_if(std::execution::par)/needle_percentile:100/real_time 3.51 ms 3.24 ms 204
for_each()/real_time 94.6 ms 94.6 ms 7
for_each(poolstl::par)/real_time 18.7 ms 0.044 ms 36
for_each(std::execution::par)/real_time 15.3 ms 12.9 ms 46
sort()/real_time 603 ms 602 ms 1
sort(poolstl::par)/real_time 112 ms 6.64 ms 6
sort(std::execution::par)/real_time 113 ms 102 ms 6
pluggable_sort(poolstl::par, ..., pdqsort)/real_time 71.7 ms 6.67 ms 10
transform()/real_time 95.0 ms 94.9 ms 7
transform(poolstl::par)/real_time 17.4 ms 0.037 ms 38
transform(std::execution::par)/real_time 15.3 ms 13.2 ms 45
exclusive_scan()/real_time 33.7 ms 33.7 ms 21
exclusive_scan(poolstl::par)/real_time 11.6 ms 0.095 ms 55
exclusive_scan(std::execution::par)/real_time 19.8 ms 15.3 ms 32
reduce()/real_time 15.2 ms 15.2 ms 46
reduce(poolstl::par)/real_time 4.06 ms 0.044 ms 169
reduce(std::execution::par)/real_time 3.38 ms 3.16 ms 214

poolSTL as std::execution::par

USE AT YOUR OWN RISK! THIS IS A HACK!

Two-line hack for missing compiler support. A no-op on compilers with support.

If POOLSTL_STD_SUPPLEMENT is defined then poolSTL will check for native compiler support. If not found then poolSTL will alias its poolstl::par as std::execution::par:

#definePOOLSTL_STD_SUPPLEMENT
#include<poolstl/poolstl.hpp>

Now just use std::execution::par as normal, and poolSTL will fill in as necessary. See supplement_test.cpp.

Example use case: You can link against TBB, so you'll use native support on GCC 9+, Clang, MSVC, etc. PoolSTL will fill in automatically on GCC <9 and Apple Clang.

Example use case 2: You'd prefer to use the TBB version, but don't want to fail on systems that don't have it. Simply use the supplement as above, but have your build system (CMake, meson, etc.) check for TBB. If not found, define POOLSTL_STD_SUPPLEMENT_NO_INCLUDE and the supplement will not #include <execution> (and neither should your code!), thus dropping the TBB link requirement. The poolSTL supplement fills in.
See the supplement section of tests/CMakeLists.txt for an example.

About

Light and self-contained implementation of C++17 parallel algorithms.

Topics

Resources

Stars

38 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

testscodecov

poolSTL

Light, self-contained, thread pool-based implementation of C++17 parallel standard library algorithms.

C++17 introduced parallel overloads of standard library algorithms that accept an Execution Policy as the first argument. Policies specify limits on how the implementation may parallelize the algorithm, enabling methods like threads, vectorization, or even GPU. Policies can be supplied by the compiler or by libraries like this one.

std::sort(std::execution::par, vec.begin(), vec.end());
// ^^^^^^^^^^^^^^^^^^^ native C++17 parallel Execution Policy 

Unfortunately compiler support varies. Quick summary of compilers' default standard libraries:

LinuxmacOSWindows
GCC 9+TBB RequiredTBB RequiredTBB Required
GCC 8-
Clang (libc++)
Clang (libstdc++)TBB RequiredTBB RequiredTBB Required
Apple Clang
MSVC 15.7+ (2017)
Parallel STLTBB RequiredTBB RequiredTBB Required
poolSTL✅*✅*✅*

PoolSTL is a supplement to fill in the support gaps. It is not a full implementation; only the basics are covered. However, it is small, easy to integrate, and has no external dependencies. A good backup to the other options.

Use poolSTL exclusively, or only on platforms lacking native support, or only if TBB is not present.

Supports C++11 and higher. Algorithms introduced in C++17 require C++17 or higher.
Tested in CI on GCC 7+, Clang/LLVM 5+, Apple Clang, MSVC, MinGW, and Emscripten.

Implemented Algorithms

Algorithms are added on an as-needed basis. If you need one open an issue or contribute a PR.
Limitations: All iterators must be random access. No nested parallel calls.

<algorithm>

<numeric>

All in std:: namespace.

Other

  • poolstl::iota_iter - Iterate over integers. Same as iterating over output of std::iota but without materializing anything. Iterator version of std::ranges::iota_view.
  • poolstl::for_each_chunk - Like std::for_each, but explicitly splits the input range into chunks then exposes the chunked parallelism. A user-specified chunk constructor is called for each parallel chunk then its output is passed to each loop iteration. Useful for workloads that need an expensive workspace that can be reused between iterations, but not simultaneously by all iterations in parallel.
  • poolstl::pluggable_sort - Like std::sort, but allows specification of sequential sort method. To parallelize pdqsort: pluggable_sort(par, v.begin(), v.end(), pdqsort).

Usage

PoolSTL provides:

  • poolstl::par: Substitute for std::execution::par. Parallelized using a thread pool.
  • poolstl::seq: Substitute for std::execution::seq. Simply calls the regular (non-policy) overload.
  • poolstl::par_if(): Choose parallel or sequential at runtime. See below.

In short, use poolstl::par to make your code parallel. Complete example:

#include<iostream>
#include<poolstl/poolstl.hpp>intmain() {
std::vector<int> v = {0, 1, 2, 3, 4, 5};
auto sum = std::reduce(poolstl::par, vec.cbegin(), vec.cend());
// ^^^^^^^^^^^^// Add this to make your code parallel.
std::cout << "Sum=" << sum << std::endl;
return0;
}

Controlling Thread Pool Size with par.on(pool)

The thread pool used by poolstl::par is managed internally by poolSTL. It is started on first use.
Use your own thread pool with poolstl::par.on(pool) for control over thread count, startup/shutdown, etc.:

task_thread_pool::task_thread_pool pool{4}; // 4 threadsstd::reduce(poolstl::par.on(pool), vec.begin(), vec.end());

Choosing Parallel or Sequential at Runtime with par_if

Sometimes the choice whether to parallelize or not should be made at runtime. For example, small datasets may not amortize the cost of starting threads, while large datasets do and should be parallelized.

Use poolstl::par_if to select between par and seq at runtime:

bool is_parallel = vec.size() > 10000;
std::reduce(poolstl::par_if(is_parallel), vec.begin(), vec.end());

Use poolstl::par_if(is_parallel, pool) to control the thread pool used by par, if selected.

Examples

Parallel for (auto& value : vec)

std::vector<int> vec = {0, 1, 2, 3, 4, 5};
// Parallel for-eachstd::for_each(poolstl::par, vec.begin(), vec.end(), [](auto& value) {
std::cout << value; // loop body
});

Parallel for (int i = 0; i < 100; ++i)

using poolstl::iota_iter;
// parallel for loopstd::for_each(poolstl::par, iota_iter<int>(0), iota_iter<int>(100), [](auto i) {
std::cout << i; // loop body
});

Parallel Sort

std::vector<int> vec = {5, 2, 1, 3, 0, 4};
std::sort(poolstl::par, vec.begin(), vec.end());

Installation

Single File

Each release publishes a single-file amalgamated poolstl.hpp. Simply copy this into your project.

Build requirements:

  • Clang and GCC 8 or older: require -lpthread to use C++11 threads.
  • Emscripten: compile and link with -pthread to use C++11 threads. See docs.

CMake

include(FetchContent)
FetchContent_Declare(
poolSTL
GIT_REPOSITORY https://github.com/alugowski/poolSTL
GIT_TAG main
GIT_SHALLOWTRUE
)
FetchContent_MakeAvailable(poolSTL)
target_link_libraries(YOUR_TARGETpoolSTL::poolSTL)

Alternatively copy or checkout the repo into your project and:

add_subdirectory(poolSTL)

Benchmark

See benchmark/ to compare poolSTL against the standard sequential implementation, and (if available) the native std::execution::par implementation.

Results on an M1 Pro (6 power, 2 efficiency cores), with GCC 13:

-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
all_of()/real_time 19.9 ms 19.9 ms 35
all_of(poolstl::par)/real_time 3.47 ms 0.119 ms 198
all_of(std::execution::par)/real_time 3.45 ms 3.25 ms 213
find_if()/needle_percentile:5/real_time 0.988 ms 0.987 ms 712
find_if()/needle_percentile:50/real_time 9.87 ms 9.86 ms 71
find_if()/needle_percentile:100/real_time 19.7 ms 19.7 ms 36
find_if(poolstl::par)/needle_percentile:5/real_time 0.405 ms 0.050 ms 1730
find_if(poolstl::par)/needle_percentile:50/real_time 1.85 ms 0.096 ms 393
find_if(poolstl::par)/needle_percentile:100/real_time 3.64 ms 0.102 ms 193
find_if(std::execution::par)/needle_percentile:5/real_time 0.230 ms 0.220 ms 3103
find_if(std::execution::par)/needle_percentile:50/real_time 1.75 ms 1.60 ms 410
find_if(std::execution::par)/needle_percentile:100/real_time 3.51 ms 3.24 ms 204
for_each()/real_time 94.6 ms 94.6 ms 7
for_each(poolstl::par)/real_time 18.7 ms 0.044 ms 36
for_each(std::execution::par)/real_time 15.3 ms 12.9 ms 46
sort()/real_time 603 ms 602 ms 1
sort(poolstl::par)/real_time 112 ms 6.64 ms 6
sort(std::execution::par)/real_time 113 ms 102 ms 6
pluggable_sort(poolstl::par, ..., pdqsort)/real_time 71.7 ms 6.67 ms 10
transform()/real_time 95.0 ms 94.9 ms 7
transform(poolstl::par)/real_time 17.4 ms 0.037 ms 38
transform(std::execution::par)/real_time 15.3 ms 13.2 ms 45
exclusive_scan()/real_time 33.7 ms 33.7 ms 21
exclusive_scan(poolstl::par)/real_time 11.6 ms 0.095 ms 55
exclusive_scan(std::execution::par)/real_time 19.8 ms 15.3 ms 32
reduce()/real_time 15.2 ms 15.2 ms 46
reduce(poolstl::par)/real_time 4.06 ms 0.044 ms 169
reduce(std::execution::par)/real_time 3.38 ms 3.16 ms 214

poolSTL as std::execution::par

USE AT YOUR OWN RISK! THIS IS A HACK!

Two-line hack for missing compiler support. A no-op on compilers with support.

If POOLSTL_STD_SUPPLEMENT is defined then poolSTL will check for native compiler support. If not found then poolSTL will alias its poolstl::par as std::execution::par:

#definePOOLSTL_STD_SUPPLEMENT
#include<poolstl/poolstl.hpp>

Now just use std::execution::par as normal, and poolSTL will fill in as necessary. See supplement_test.cpp.

Example use case: You can link against TBB, so you'll use native support on GCC 9+, Clang, MSVC, etc. PoolSTL will fill in automatically on GCC <9 and Apple Clang.

Example use case 2: You'd prefer to use the TBB version, but don't want to fail on systems that don't have it. Simply use the supplement as above, but have your build system (CMake, meson, etc.) check for TBB. If not found, define POOLSTL_STD_SUPPLEMENT_NO_INCLUDE and the supplement will not #include <execution> (and neither should your code!), thus dropping the TBB link requirement. The poolSTL supplement fills in.
See the supplement section of tests/CMakeLists.txt for an example.

About

Light and self-contained implementation of C++17 parallel algorithms.

Topics

Resources

Stars

38 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

testscodecov

poolSTL

Light, self-contained, thread pool-based implementation of C++17 parallel standard library algorithms.

C++17 introduced parallel overloads of standard library algorithms that accept an Execution Policy as the first argument. Policies specify limits on how the implementation may parallelize the algorithm, enabling methods like threads, vectorization, or even GPU. Policies can be supplied by the compiler or by libraries like this one.

std::sort(std::execution::par, vec.begin(), vec.end());
// ^^^^^^^^^^^^^^^^^^^ native C++17 parallel Execution Policy 

Unfortunately compiler support varies. Quick summary of compilers' default standard libraries:

LinuxmacOSWindows
GCC 9+TBB RequiredTBB RequiredTBB Required
GCC 8-
Clang (libc++)
Clang (libstdc++)TBB RequiredTBB RequiredTBB Required
Apple Clang
MSVC 15.7+ (2017)
Parallel STLTBB RequiredTBB RequiredTBB Required
poolSTL✅*✅*✅*

PoolSTL is a supplement to fill in the support gaps. It is not a full implementation; only the basics are covered. However, it is small, easy to integrate, and has no external dependencies. A good backup to the other options.

Use poolSTL exclusively, or only on platforms lacking native support, or only if TBB is not present.

Supports C++11 and higher. Algorithms introduced in C++17 require C++17 or higher.
Tested in CI on GCC 7+, Clang/LLVM 5+, Apple Clang, MSVC, MinGW, and Emscripten.

Implemented Algorithms

Algorithms are added on an as-needed basis. If you need one open an issue or contribute a PR.
Limitations: All iterators must be random access. No nested parallel calls.

<algorithm>

<numeric>

All in std:: namespace.

Other

  • poolstl::iota_iter - Iterate over integers. Same as iterating over output of std::iota but without materializing anything. Iterator version of std::ranges::iota_view.
  • poolstl::for_each_chunk - Like std::for_each, but explicitly splits the input range into chunks then exposes the chunked parallelism. A user-specified chunk constructor is called for each parallel chunk then its output is passed to each loop iteration. Useful for workloads that need an expensive workspace that can be reused between iterations, but not simultaneously by all iterations in parallel.
  • poolstl::pluggable_sort - Like std::sort, but allows specification of sequential sort method. To parallelize pdqsort: pluggable_sort(par, v.begin(), v.end(), pdqsort).

Usage

PoolSTL provides:

  • poolstl::par: Substitute for std::execution::par. Parallelized using a thread pool.
  • poolstl::seq: Substitute for std::execution::seq. Simply calls the regular (non-policy) overload.
  • poolstl::par_if(): Choose parallel or sequential at runtime. See below.

In short, use poolstl::par to make your code parallel. Complete example:

#include<iostream>
#include<poolstl/poolstl.hpp>intmain() {
std::vector<int> v = {0, 1, 2, 3, 4, 5};
auto sum = std::reduce(poolstl::par, vec.cbegin(), vec.cend());
// ^^^^^^^^^^^^// Add this to make your code parallel.
std::cout << "Sum=" << sum << std::endl;
return0;
}

Controlling Thread Pool Size with par.on(pool)

The thread pool used by poolstl::par is managed internally by poolSTL. It is started on first use.
Use your own thread pool with poolstl::par.on(pool) for control over thread count, startup/shutdown, etc.:

task_thread_pool::task_thread_pool pool{4}; // 4 threadsstd::reduce(poolstl::par.on(pool), vec.begin(), vec.end());

Choosing Parallel or Sequential at Runtime with par_if

Sometimes the choice whether to parallelize or not should be made at runtime. For example, small datasets may not amortize the cost of starting threads, while large datasets do and should be parallelized.

Use poolstl::par_if to select between par and seq at runtime:

bool is_parallel = vec.size() > 10000;
std::reduce(poolstl::par_if(is_parallel), vec.begin(), vec.end());

Use poolstl::par_if(is_parallel, pool) to control the thread pool used by par, if selected.

Examples

Parallel for (auto& value : vec)

std::vector<int> vec = {0, 1, 2, 3, 4, 5};
// Parallel for-eachstd::for_each(poolstl::par, vec.begin(), vec.end(), [](auto& value) {
std::cout << value; // loop body
});

Parallel for (int i = 0; i < 100; ++i)

using poolstl::iota_iter;
// parallel for loopstd::for_each(poolstl::par, iota_iter<int>(0), iota_iter<int>(100), [](auto i) {
std::cout << i; // loop body
});

Parallel Sort

std::vector<int> vec = {5, 2, 1, 3, 0, 4};
std::sort(poolstl::par, vec.begin(), vec.end());

Installation

Single File

Each release publishes a single-file amalgamated poolstl.hpp. Simply copy this into your project.

Build requirements:

  • Clang and GCC 8 or older: require -lpthread to use C++11 threads.
  • Emscripten: compile and link with -pthread to use C++11 threads. See docs.

CMake

include(FetchContent)
FetchContent_Declare(
poolSTL
GIT_REPOSITORY https://github.com/alugowski/poolSTL
GIT_TAG main
GIT_SHALLOWTRUE
)
FetchContent_MakeAvailable(poolSTL)
target_link_libraries(YOUR_TARGETpoolSTL::poolSTL)

Alternatively copy or checkout the repo into your project and:

add_subdirectory(poolSTL)

Benchmark

See benchmark/ to compare poolSTL against the standard sequential implementation, and (if available) the native std::execution::par implementation.

Results on an M1 Pro (6 power, 2 efficiency cores), with GCC 13:

-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
all_of()/real_time 19.9 ms 19.9 ms 35
all_of(poolstl::par)/real_time 3.47 ms 0.119 ms 198
all_of(std::execution::par)/real_time 3.45 ms 3.25 ms 213
find_if()/needle_percentile:5/real_time 0.988 ms 0.987 ms 712
find_if()/needle_percentile:50/real_time 9.87 ms 9.86 ms 71
find_if()/needle_percentile:100/real_time 19.7 ms 19.7 ms 36
find_if(poolstl::par)/needle_percentile:5/real_time 0.405 ms 0.050 ms 1730
find_if(poolstl::par)/needle_percentile:50/real_time 1.85 ms 0.096 ms 393
find_if(poolstl::par)/needle_percentile:100/real_time 3.64 ms 0.102 ms 193
find_if(std::execution::par)/needle_percentile:5/real_time 0.230 ms 0.220 ms 3103
find_if(std::execution::par)/needle_percentile:50/real_time 1.75 ms 1.60 ms 410
find_if(std::execution::par)/needle_percentile:100/real_time 3.51 ms 3.24 ms 204
for_each()/real_time 94.6 ms 94.6 ms 7
for_each(poolstl::par)/real_time 18.7 ms 0.044 ms 36
for_each(std::execution::par)/real_time 15.3 ms 12.9 ms 46
sort()/real_time 603 ms 602 ms 1
sort(poolstl::par)/real_time 112 ms 6.64 ms 6
sort(std::execution::par)/real_time 113 ms 102 ms 6
pluggable_sort(poolstl::par, ..., pdqsort)/real_time 71.7 ms 6.67 ms 10
transform()/real_time 95.0 ms 94.9 ms 7
transform(poolstl::par)/real_time 17.4 ms 0.037 ms 38
transform(std::execution::par)/real_time 15.3 ms 13.2 ms 45
exclusive_scan()/real_time 33.7 ms 33.7 ms 21
exclusive_scan(poolstl::par)/real_time 11.6 ms 0.095 ms 55
exclusive_scan(std::execution::par)/real_time 19.8 ms 15.3 ms 32
reduce()/real_time 15.2 ms 15.2 ms 46
reduce(poolstl::par)/real_time 4.06 ms 0.044 ms 169
reduce(std::execution::par)/real_time 3.38 ms 3.16 ms 214

poolSTL as std::execution::par

USE AT YOUR OWN RISK! THIS IS A HACK!

Two-line hack for missing compiler support. A no-op on compilers with support.

If POOLSTL_STD_SUPPLEMENT is defined then poolSTL will check for native compiler support. If not found then poolSTL will alias its poolstl::par as std::execution::par:

#definePOOLSTL_STD_SUPPLEMENT
#include<poolstl/poolstl.hpp>

Now just use std::execution::par as normal, and poolSTL will fill in as necessary. See supplement_test.cpp.

Example use case: You can link against TBB, so you'll use native support on GCC 9+, Clang, MSVC, etc. PoolSTL will fill in automatically on GCC <9 and Apple Clang.

Example use case 2: You'd prefer to use the TBB version, but don't want to fail on systems that don't have it. Simply use the supplement as above, but have your build system (CMake, meson, etc.) check for TBB. If not found, define POOLSTL_STD_SUPPLEMENT_NO_INCLUDE and the supplement will not #include <execution> (and neither should your code!), thus dropping the TBB link requirement. The poolSTL supplement fills in.
See the supplement section of tests/CMakeLists.txt for an example.

About

Light and self-contained implementation of C++17 parallel algorithms.

Topics

Resources

Stars

38 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

testscodecov

poolSTL

Light, self-contained, thread pool-based implementation of C++17 parallel standard library algorithms.

C++17 introduced parallel overloads of standard library algorithms that accept an Execution Policy as the first argument. Policies specify limits on how the implementation may parallelize the algorithm, enabling methods like threads, vectorization, or even GPU. Policies can be supplied by the compiler or by libraries like this one.

std::sort(std::execution::par, vec.begin(), vec.end());
// ^^^^^^^^^^^^^^^^^^^ native C++17 parallel Execution Policy 

Unfortunately compiler support varies. Quick summary of compilers' default standard libraries:

LinuxmacOSWindows
GCC 9+TBB RequiredTBB RequiredTBB Required
GCC 8-
Clang (libc++)
Clang (libstdc++)TBB RequiredTBB RequiredTBB Required
Apple Clang
MSVC 15.7+ (2017)
Parallel STLTBB RequiredTBB RequiredTBB Required
poolSTL✅*✅*✅*

PoolSTL is a supplement to fill in the support gaps. It is not a full implementation; only the basics are covered. However, it is small, easy to integrate, and has no external dependencies. A good backup to the other options.

Use poolSTL exclusively, or only on platforms lacking native support, or only if TBB is not present.

Supports C++11 and higher. Algorithms introduced in C++17 require C++17 or higher.
Tested in CI on GCC 7+, Clang/LLVM 5+, Apple Clang, MSVC, MinGW, and Emscripten.

Implemented Algorithms

Algorithms are added on an as-needed basis. If you need one open an issue or contribute a PR.
Limitations: All iterators must be random access. No nested parallel calls.

<algorithm>

<numeric>

All in std:: namespace.

Other

  • poolstl::iota_iter - Iterate over integers. Same as iterating over output of std::iota but without materializing anything. Iterator version of std::ranges::iota_view.
  • poolstl::for_each_chunk - Like std::for_each, but explicitly splits the input range into chunks then exposes the chunked parallelism. A user-specified chunk constructor is called for each parallel chunk then its output is passed to each loop iteration. Useful for workloads that need an expensive workspace that can be reused between iterations, but not simultaneously by all iterations in parallel.
  • poolstl::pluggable_sort - Like std::sort, but allows specification of sequential sort method. To parallelize pdqsort: pluggable_sort(par, v.begin(), v.end(), pdqsort).

Usage

PoolSTL provides:

  • poolstl::par: Substitute for std::execution::par. Parallelized using a thread pool.
  • poolstl::seq: Substitute for std::execution::seq. Simply calls the regular (non-policy) overload.
  • poolstl::par_if(): Choose parallel or sequential at runtime. See below.

In short, use poolstl::par to make your code parallel. Complete example:

#include<iostream>
#include<poolstl/poolstl.hpp>intmain() {
std::vector<int> v = {0, 1, 2, 3, 4, 5};
auto sum = std::reduce(poolstl::par, vec.cbegin(), vec.cend());
// ^^^^^^^^^^^^// Add this to make your code parallel.
std::cout << "Sum=" << sum << std::endl;
return0;
}

Controlling Thread Pool Size with par.on(pool)

The thread pool used by poolstl::par is managed internally by poolSTL. It is started on first use.
Use your own thread pool with poolstl::par.on(pool) for control over thread count, startup/shutdown, etc.:

task_thread_pool::task_thread_pool pool{4}; // 4 threadsstd::reduce(poolstl::par.on(pool), vec.begin(), vec.end());

Choosing Parallel or Sequential at Runtime with par_if

Sometimes the choice whether to parallelize or not should be made at runtime. For example, small datasets may not amortize the cost of starting threads, while large datasets do and should be parallelized.

Use poolstl::par_if to select between par and seq at runtime:

bool is_parallel = vec.size() > 10000;
std::reduce(poolstl::par_if(is_parallel), vec.begin(), vec.end());

Use poolstl::par_if(is_parallel, pool) to control the thread pool used by par, if selected.

Examples

Parallel for (auto& value : vec)

std::vector<int> vec = {0, 1, 2, 3, 4, 5};
// Parallel for-eachstd::for_each(poolstl::par, vec.begin(), vec.end(), [](auto& value) {
std::cout << value; // loop body
});

Parallel for (int i = 0; i < 100; ++i)

using poolstl::iota_iter;
// parallel for loopstd::for_each(poolstl::par, iota_iter<int>(0), iota_iter<int>(100), [](auto i) {
std::cout << i; // loop body
});

Parallel Sort

std::vector<int> vec = {5, 2, 1, 3, 0, 4};
std::sort(poolstl::par, vec.begin(), vec.end());

Installation

Single File

Each release publishes a single-file amalgamated poolstl.hpp. Simply copy this into your project.

Build requirements:

  • Clang and GCC 8 or older: require -lpthread to use C++11 threads.
  • Emscripten: compile and link with -pthread to use C++11 threads. See docs.

CMake

include(FetchContent)
FetchContent_Declare(
poolSTL
GIT_REPOSITORY https://github.com/alugowski/poolSTL
GIT_TAG main
GIT_SHALLOWTRUE
)
FetchContent_MakeAvailable(poolSTL)
target_link_libraries(YOUR_TARGETpoolSTL::poolSTL)

Alternatively copy or checkout the repo into your project and:

add_subdirectory(poolSTL)

Benchmark

See benchmark/ to compare poolSTL against the standard sequential implementation, and (if available) the native std::execution::par implementation.

Results on an M1 Pro (6 power, 2 efficiency cores), with GCC 13:

-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
all_of()/real_time 19.9 ms 19.9 ms 35
all_of(poolstl::par)/real_time 3.47 ms 0.119 ms 198
all_of(std::execution::par)/real_time 3.45 ms 3.25 ms 213
find_if()/needle_percentile:5/real_time 0.988 ms 0.987 ms 712
find_if()/needle_percentile:50/real_time 9.87 ms 9.86 ms 71
find_if()/needle_percentile:100/real_time 19.7 ms 19.7 ms 36
find_if(poolstl::par)/needle_percentile:5/real_time 0.405 ms 0.050 ms 1730
find_if(poolstl::par)/needle_percentile:50/real_time 1.85 ms 0.096 ms 393
find_if(poolstl::par)/needle_percentile:100/real_time 3.64 ms 0.102 ms 193
find_if(std::execution::par)/needle_percentile:5/real_time 0.230 ms 0.220 ms 3103
find_if(std::execution::par)/needle_percentile:50/real_time 1.75 ms 1.60 ms 410
find_if(std::execution::par)/needle_percentile:100/real_time 3.51 ms 3.24 ms 204
for_each()/real_time 94.6 ms 94.6 ms 7
for_each(poolstl::par)/real_time 18.7 ms 0.044 ms 36
for_each(std::execution::par)/real_time 15.3 ms 12.9 ms 46
sort()/real_time 603 ms 602 ms 1
sort(poolstl::par)/real_time 112 ms 6.64 ms 6
sort(std::execution::par)/real_time 113 ms 102 ms 6
pluggable_sort(poolstl::par, ..., pdqsort)/real_time 71.7 ms 6.67 ms 10
transform()/real_time 95.0 ms 94.9 ms 7
transform(poolstl::par)/real_time 17.4 ms 0.037 ms 38
transform(std::execution::par)/real_time 15.3 ms 13.2 ms 45
exclusive_scan()/real_time 33.7 ms 33.7 ms 21
exclusive_scan(poolstl::par)/real_time 11.6 ms 0.095 ms 55
exclusive_scan(std::execution::par)/real_time 19.8 ms 15.3 ms 32
reduce()/real_time 15.2 ms 15.2 ms 46
reduce(poolstl::par)/real_time 4.06 ms 0.044 ms 169
reduce(std::execution::par)/real_time 3.38 ms 3.16 ms 214

poolSTL as std::execution::par

USE AT YOUR OWN RISK! THIS IS A HACK!

Two-line hack for missing compiler support. A no-op on compilers with support.

If POOLSTL_STD_SUPPLEMENT is defined then poolSTL will check for native compiler support. If not found then poolSTL will alias its poolstl::par as std::execution::par:

#definePOOLSTL_STD_SUPPLEMENT
#include<poolstl/poolstl.hpp>

Now just use std::execution::par as normal, and poolSTL will fill in as necessary. See supplement_test.cpp.

Example use case: You can link against TBB, so you'll use native support on GCC 9+, Clang, MSVC, etc. PoolSTL will fill in automatically on GCC <9 and Apple Clang.

Example use case 2: You'd prefer to use the TBB version, but don't want to fail on systems that don't have it. Simply use the supplement as above, but have your build system (CMake, meson, etc.) check for TBB. If not found, define POOLSTL_STD_SUPPLEMENT_NO_INCLUDE and the supplement will not #include <execution> (and neither should your code!), thus dropping the TBB link requirement. The poolSTL supplement fills in.
See the supplement section of tests/CMakeLists.txt for an example.

About

Light and self-contained implementation of C++17 parallel algorithms.

Topics

Resources

Stars

38 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

testscodecov

poolSTL

Light, self-contained, thread pool-based implementation of C++17 parallel standard library algorithms.

C++17 introduced parallel overloads of standard library algorithms that accept an Execution Policy as the first argument. Policies specify limits on how the implementation may parallelize the algorithm, enabling methods like threads, vectorization, or even GPU. Policies can be supplied by the compiler or by libraries like this one.

std::sort(std::execution::par, vec.begin(), vec.end());
// ^^^^^^^^^^^^^^^^^^^ native C++17 parallel Execution Policy 

Unfortunately compiler support varies. Quick summary of compilers' default standard libraries:

LinuxmacOSWindows
GCC 9+TBB RequiredTBB RequiredTBB Required
GCC 8-
Clang (libc++)
Clang (libstdc++)TBB RequiredTBB RequiredTBB Required
Apple Clang
MSVC 15.7+ (2017)
Parallel STLTBB RequiredTBB RequiredTBB Required
poolSTL✅*✅*✅*

PoolSTL is a supplement to fill in the support gaps. It is not a full implementation; only the basics are covered. However, it is small, easy to integrate, and has no external dependencies. A good backup to the other options.

Use poolSTL exclusively, or only on platforms lacking native support, or only if TBB is not present.

Supports C++11 and higher. Algorithms introduced in C++17 require C++17 or higher.
Tested in CI on GCC 7+, Clang/LLVM 5+, Apple Clang, MSVC, MinGW, and Emscripten.

Implemented Algorithms

Algorithms are added on an as-needed basis. If you need one open an issue or contribute a PR.
Limitations: All iterators must be random access. No nested parallel calls.

<algorithm>

<numeric>

All in std:: namespace.

Other

  • poolstl::iota_iter - Iterate over integers. Same as iterating over output of std::iota but without materializing anything. Iterator version of std::ranges::iota_view.
  • poolstl::for_each_chunk - Like std::for_each, but explicitly splits the input range into chunks then exposes the chunked parallelism. A user-specified chunk constructor is called for each parallel chunk then its output is passed to each loop iteration. Useful for workloads that need an expensive workspace that can be reused between iterations, but not simultaneously by all iterations in parallel.
  • poolstl::pluggable_sort - Like std::sort, but allows specification of sequential sort method. To parallelize pdqsort: pluggable_sort(par, v.begin(), v.end(), pdqsort).

Usage

PoolSTL provides:

  • poolstl::par: Substitute for std::execution::par. Parallelized using a thread pool.
  • poolstl::seq: Substitute for std::execution::seq. Simply calls the regular (non-policy) overload.
  • poolstl::par_if(): Choose parallel or sequential at runtime. See below.

In short, use poolstl::par to make your code parallel. Complete example:

#include<iostream>
#include<poolstl/poolstl.hpp>intmain() {
std::vector<int> v = {0, 1, 2, 3, 4, 5};
auto sum = std::reduce(poolstl::par, vec.cbegin(), vec.cend());
// ^^^^^^^^^^^^// Add this to make your code parallel.
std::cout << "Sum=" << sum << std::endl;
return0;
}

Controlling Thread Pool Size with par.on(pool)

The thread pool used by poolstl::par is managed internally by poolSTL. It is started on first use.
Use your own thread pool with poolstl::par.on(pool) for control over thread count, startup/shutdown, etc.:

task_thread_pool::task_thread_pool pool{4}; // 4 threadsstd::reduce(poolstl::par.on(pool), vec.begin(), vec.end());

Choosing Parallel or Sequential at Runtime with par_if

Sometimes the choice whether to parallelize or not should be made at runtime. For example, small datasets may not amortize the cost of starting threads, while large datasets do and should be parallelized.

Use poolstl::par_if to select between par and seq at runtime:

bool is_parallel = vec.size() > 10000;
std::reduce(poolstl::par_if(is_parallel), vec.begin(), vec.end());

Use poolstl::par_if(is_parallel, pool) to control the thread pool used by par, if selected.

Examples

Parallel for (auto& value : vec)

std::vector<int> vec = {0, 1, 2, 3, 4, 5};
// Parallel for-eachstd::for_each(poolstl::par, vec.begin(), vec.end(), [](auto& value) {
std::cout << value; // loop body
});

Parallel for (int i = 0; i < 100; ++i)

using poolstl::iota_iter;
// parallel for loopstd::for_each(poolstl::par, iota_iter<int>(0), iota_iter<int>(100), [](auto i) {
std::cout << i; // loop body
});

Parallel Sort

std::vector<int> vec = {5, 2, 1, 3, 0, 4};
std::sort(poolstl::par, vec.begin(), vec.end());

Installation

Single File

Each release publishes a single-file amalgamated poolstl.hpp. Simply copy this into your project.

Build requirements:

  • Clang and GCC 8 or older: require -lpthread to use C++11 threads.
  • Emscripten: compile and link with -pthread to use C++11 threads. See docs.

CMake

include(FetchContent)
FetchContent_Declare(
poolSTL
GIT_REPOSITORY https://github.com/alugowski/poolSTL
GIT_TAG main
GIT_SHALLOWTRUE
)
FetchContent_MakeAvailable(poolSTL)
target_link_libraries(YOUR_TARGETpoolSTL::poolSTL)

Alternatively copy or checkout the repo into your project and:

add_subdirectory(poolSTL)

Benchmark

See benchmark/ to compare poolSTL against the standard sequential implementation, and (if available) the native std::execution::par implementation.

Results on an M1 Pro (6 power, 2 efficiency cores), with GCC 13:

-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
all_of()/real_time 19.9 ms 19.9 ms 35
all_of(poolstl::par)/real_time 3.47 ms 0.119 ms 198
all_of(std::execution::par)/real_time 3.45 ms 3.25 ms 213
find_if()/needle_percentile:5/real_time 0.988 ms 0.987 ms 712
find_if()/needle_percentile:50/real_time 9.87 ms 9.86 ms 71
find_if()/needle_percentile:100/real_time 19.7 ms 19.7 ms 36
find_if(poolstl::par)/needle_percentile:5/real_time 0.405 ms 0.050 ms 1730
find_if(poolstl::par)/needle_percentile:50/real_time 1.85 ms 0.096 ms 393
find_if(poolstl::par)/needle_percentile:100/real_time 3.64 ms 0.102 ms 193
find_if(std::execution::par)/needle_percentile:5/real_time 0.230 ms 0.220 ms 3103
find_if(std::execution::par)/needle_percentile:50/real_time 1.75 ms 1.60 ms 410
find_if(std::execution::par)/needle_percentile:100/real_time 3.51 ms 3.24 ms 204
for_each()/real_time 94.6 ms 94.6 ms 7
for_each(poolstl::par)/real_time 18.7 ms 0.044 ms 36
for_each(std::execution::par)/real_time 15.3 ms 12.9 ms 46
sort()/real_time 603 ms 602 ms 1
sort(poolstl::par)/real_time 112 ms 6.64 ms 6
sort(std::execution::par)/real_time 113 ms 102 ms 6
pluggable_sort(poolstl::par, ..., pdqsort)/real_time 71.7 ms 6.67 ms 10
transform()/real_time 95.0 ms 94.9 ms 7
transform(poolstl::par)/real_time 17.4 ms 0.037 ms 38
transform(std::execution::par)/real_time 15.3 ms 13.2 ms 45
exclusive_scan()/real_time 33.7 ms 33.7 ms 21
exclusive_scan(poolstl::par)/real_time 11.6 ms 0.095 ms 55
exclusive_scan(std::execution::par)/real_time 19.8 ms 15.3 ms 32
reduce()/real_time 15.2 ms 15.2 ms 46
reduce(poolstl::par)/real_time 4.06 ms 0.044 ms 169
reduce(std::execution::par)/real_time 3.38 ms 3.16 ms 214

poolSTL as std::execution::par

USE AT YOUR OWN RISK! THIS IS A HACK!

Two-line hack for missing compiler support. A no-op on compilers with support.

If POOLSTL_STD_SUPPLEMENT is defined then poolSTL will check for native compiler support. If not found then poolSTL will alias its poolstl::par as std::execution::par:

#definePOOLSTL_STD_SUPPLEMENT
#include<poolstl/poolstl.hpp>

Now just use std::execution::par as normal, and poolSTL will fill in as necessary. See supplement_test.cpp.

Example use case: You can link against TBB, so you'll use native support on GCC 9+, Clang, MSVC, etc. PoolSTL will fill in automatically on GCC <9 and Apple Clang.

Example use case 2: You'd prefer to use the TBB version, but don't want to fail on systems that don't have it. Simply use the supplement as above, but have your build system (CMake, meson, etc.) check for TBB. If not found, define POOLSTL_STD_SUPPLEMENT_NO_INCLUDE and the supplement will not #include <execution> (and neither should your code!), thus dropping the TBB link requirement. The poolSTL supplement fills in.
See the supplement section of tests/CMakeLists.txt for an example.

About

Light and self-contained implementation of C++17 parallel algorithms.

Topics

Resources

Stars

38 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages