subpar
Substitutable parallelization for C++ libraries
Loading...
Searching...
No Matches
range.hpp
Go to the documentation of this file.
1#ifndef SUBPAR_RANGE_HPP
2#define SUBPAR_RANGE_HPP
3
4#include <limits>
5#include <type_traits>
6
7#ifndef SUBPAR_CUSTOM_PARALLELIZE_RANGE
8#include <vector>
9#include <stdexcept>
10#include <exception>
11#include <thread>
12#endif
13
14#include "sanisizer/sanisizer.hpp"
15
21namespace subpar {
22
40template<typename Task_>
41int sanitize_num_workers(const int num_workers, const Task_ num_tasks) {
42 // This code mirrors the return logic in the default parallelize_range(), but would be an upper bound even with a custom SUBPAR_CUSTOM_PARALLELIZE_RANGE.
43 // Remember that run_task_range must be called with a non-empty range so if num_tasks = 0, there is no choice but to not perform any calls.
44 // Similarly, we can't perform more than one call if num_tasks = 1, and we can't perform more calls than there are workers.
45
46 if (num_tasks <= 0) {
47 return 0;
48 }
49
50 if (num_workers <= 1 || num_tasks == 1) {
51 return 1;
52 }
53
54 return sanisizer::min(num_workers, num_tasks);
55}
56
124template<bool nothrow_ = false, typename Task_, class Run_>
125int parallelize_range(int num_workers, const Task_ num_tasks, const Run_ run_task_range) {
126#ifdef SUBPAR_CUSTOM_PARALLELIZE_RANGE
127 if constexpr(nothrow_) {
128#ifdef SUBPAR_CUSTOM_PARALLELIZE_RANGE_NOTHROW
129 return SUBPAR_CUSTOM_PARALLELIZE_RANGE_NOTHROW(num_workers, num_tasks, run_task_range);
130#else
131 return SUBPAR_CUSTOM_PARALLELIZE_RANGE(num_workers, num_tasks, run_task_range);
132#endif
133 } else {
134 return SUBPAR_CUSTOM_PARALLELIZE_RANGE(num_workers, num_tasks, run_task_range);
135 }
136
137#else
138 if (num_tasks <= 0) {
139 return 0;
140 }
141
142 if (num_workers <= 1 || num_tasks == 1) {
143 run_task_range(0, 0, num_tasks);
144 return 1;
145 }
146
147 // All workers with indices below 'remainder' get an extra task to fill up the remainder.
148 Task_ tasks_per_worker = 1;
149 int remainder = 0;
150 if (sanisizer::is_greater_than_or_equal(num_workers, num_tasks)) {
151 num_workers = num_tasks;
152 } else {
153 tasks_per_worker = num_tasks / num_workers;
154 remainder = num_tasks % num_workers;
155 }
156
157 const auto get_start = [&tasks_per_worker,&remainder](const int w) -> Task_ {
158 // Need to shift the start by the number of previous 'w' that added a remainder.
159 return w * tasks_per_worker + (w < remainder ? w : remainder);
160 };
161
162 const auto get_length = [&tasks_per_worker,&remainder](const int w) -> Task_ {
163 return tasks_per_worker + (w < remainder);
164 };
165
166 // Avoid instantiating a vector if it is known that the function can't throw.
167 auto errors = [&]{
168 if constexpr(nothrow_) {
169 return true;
170 } else {
171 return sanisizer::create<std::vector<std::exception_ptr> >(num_workers);
172 }
173 }();
174
175#if defined(_OPENMP) && !defined(SUBPAR_NO_OPENMP_RANGE) && !defined(SUBPAR_NO_OPENMP)
176#define SUBPAR_USES_OPENMP 1
177#define SUBPAR_USES_OPENMP_RANGE 1
178
179 // OpenMP doesn't guarantee that we'll actually start the specified number of workers,
180 // so we need to do a loop here to ensure that each task range is executed.
181 #pragma omp parallel for num_threads(num_workers)
182 for (int w = 0; w < num_workers; ++w) {
183 const Task_ start = get_start(w);
184 const Task_ length = get_length(w);
185
186 if constexpr(nothrow_) {
187 run_task_range(w, start, length);
188 } else {
189 try {
190 run_task_range(w, start, length);
191 } catch (...) {
192 errors[w] = std::current_exception();
193 }
194 }
195 }
196
197#else
198// Wiping it out, just in case.
199#undef SUBPAR_USES_OPENMP
200#undef SUBPAR_USES_OPENMP_RANGE
201
202 // We run the first job on the current thread, to avoid having to spin up an unnecessary worker.
203 std::vector<std::thread> workers;
204 sanisizer::reserve(workers, num_workers - 1); // preallocate to ensure we don't get alloc errors during emplace_back().
205
206 for (int w = 1; w < num_workers; ++w) {
207 const Task_ start = get_start(w);
208 const Task_ length = get_length(w);
209
210 if constexpr(nothrow_) {
211 workers.emplace_back(run_task_range, w, start, length);
212 } else {
213 workers.emplace_back([&run_task_range,&errors](int w, Task_ start, Task_ length) -> void {
214 try {
215 run_task_range(w, start, length);
216 } catch (...) {
217 errors[w] = std::current_exception();
218 }
219 }, w, start, length);
220 }
221 }
222
223 {
224 const Task_ start = get_start(0);
225 const Task_ length = get_length(0);
226
227 if constexpr(nothrow_) {
228 run_task_range(0, start, length);
229 } else {
230 try {
231 run_task_range(0, start, length);
232 } catch (...) {
233 errors[0] = std::current_exception();
234 }
235 }
236 }
237
238 for (auto& wrk : workers) {
239 wrk.join();
240 }
241#endif
242
243 if constexpr(!nothrow_) {
244 for (const auto& e : errors) {
245 if (e) {
246 std::rethrow_exception(e);
247 }
248 }
249 }
250#endif
251
252 return num_workers;
253}
254
258// Back-compatibility only.
259template<typename Task_, class Run_>
260void parallelize(int num_workers, Task_ num_tasks, Run_ run_task_range) {
261 parallelize_range<false, Task_, Run_>(num_workers, num_tasks, std::move(run_task_range));
262}
267}
268
269#endif
Substitutable parallelization functions.
int sanitize_num_workers(const int num_workers, const Task_ num_tasks)
Adjust the number of workers to the number of tasks in parallelize_range().
Definition range.hpp:41
int parallelize_range(int num_workers, const Task_ num_tasks, const Run_ run_task_range)
Parallelize a range of tasks across multiple workers.
Definition range.hpp:125