Loading...
Searching...
No Matches
threading.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Thread-team sizing for the OpenMP regions in Q3's hot loops.
5 *
6 * Every `#pragma omp parallel for` in this codebase carries
7 * `num_threads(...) if(... > 1)` with a count from `thread_count_for()`,
8 * rather than letting the runtime pick. Two reasons:
9 *
10 * - **Zero overhead at one thread.** When the computed count is 1 the `if`
11 * clause is false, so the runtime runs the region inline in the
12 * encountering thread: no team is forked, no barrier is taken, and the
13 * build behaves as if the pragma were not there. That is what makes a
14 * `OMP_NUM_THREADS=1` run of an OpenMP build match a build without it.
15 * - **A team has to earn its keep.** Forking and joining costs a few
16 * microseconds; a loop over a few hundred grid points does not. Each call
17 * site states the smallest slice of work worth handing to a thread and
18 * `thread_count_for()` refuses to start more threads than that allows.
19 *
20 * Without OpenMP the whole header collapses to constants, so callers need no
21 * `#ifdef` of their own.
22 */
23
24#include <algorithm>
25
26#ifdef _OPENMP
27#include <omp.h>
28#endif
29
30namespace q3 {
31namespace execution {
32namespace threading {
33
34/**
35 * Whether this translation unit was compiled with OpenMP enabled.
36 *
37 * Reported in the run header so a log says which binary produced it, and used
38 * by tests that assert the single-thread path is taken either way.
39 */
40[[nodiscard]] inline constexpr bool is_compiled_with_openmp() {
41#ifdef _OPENMP
42 return true;
43#else
44 return false;
45#endif
46}
47
48/**
49 * The largest team a parallel region could be given here, which is what
50 * `OMP_NUM_THREADS` sets. Always 1 without OpenMP.
51 */
52[[nodiscard]] inline int maximum_threads() {
53#ifdef _OPENMP
54 return omp_get_max_threads();
55#else
56 return 1;
57#endif
58}
59
60/**
61 * The calling thread's index inside the enclosing parallel region, or 0 when
62 * there is none. Used to pick a per-thread scratch slot, which is also why it
63 * must stay a plain index into a dense range rather than an opaque id.
64 */
65[[nodiscard]] inline int thread_index() {
66#ifdef _OPENMP
67 return omp_get_thread_num();
68#else
69 return 0;
70#endif
71}
72
73/**
74 * The size of the team actually running the enclosing parallel region, or 1
75 * outside one.
76 *
77 * `num_threads(n)` is a request, not a promise -- a runtime with dynamic
78 * adjustment on may hand back a smaller team. Code that splits an array into
79 * one slice per thread must divide by the team it got, not the team it asked
80 * for, or the slices at the end belong to threads that never ran and the work
81 * is silently skipped.
82 */
83[[nodiscard]] inline int team_size() {
84#ifdef _OPENMP
85 return omp_get_num_threads();
86#else
87 return 1;
88#endif
89}
90
91/**
92 * Whether the caller is already inside a parallel region.
93 *
94 * Hot loops nest -- the band loop in the Hamiltonian applicator contains the
95 * scatter/gather loops -- and an inner region that forked its own team would
96 * oversubscribe the machine. Inner call sites gate on this and stay serial
97 * when an outer region has already claimed the cores.
98 */
99[[nodiscard]] inline bool is_inside_parallel_region() {
100#ifdef _OPENMP
101 return omp_in_parallel() != 0;
102#else
103 return false;
104#endif
105}
106
107/**
108 * How many threads to ask for over `work_items`, given that a thread is not
109 * worth starting until it has `minimum_items_per_thread` of work to do.
110 *
111 * Returns 1 -- meaning "run this region serially, do not fork" -- when OpenMP
112 * is off, when `OMP_NUM_THREADS` is 1, when an outer region already holds the
113 * cores, or when the loop is too short to pay for a team. Never returns more
114 * than `maximum_threads()`.
115 */
116[[nodiscard]] inline int thread_count_for(long long work_items,
117 long long minimum_items_per_thread) {
118 if (work_items <= 0 || minimum_items_per_thread <= 0) {
119 return 1;
120 }
122 return 1;
123 }
124 long long affordable_threads = work_items / minimum_items_per_thread;
125 if (affordable_threads < 2) {
126 return 1;
127 }
128 long long available_threads = static_cast<long long>(maximum_threads());
129 return static_cast<int>(std::min(affordable_threads, available_threads));
130}
131
132/**
133 * The half-open slice of `[0, work_items)` that `slice_index` of
134 * `number_of_slices` owns, written into `first` and `last`.
135 *
136 * Deterministic and independent of how the slices are then scheduled, which is
137 * what lets a threaded reduction stay bit-identical: a thread always sums the
138 * same contiguous run of terms in the same order, and the partial sums are
139 * combined by slice index afterwards. The first `work_items % number_of_slices`
140 * slices take one extra item.
141 */
142inline void slice_bounds(long long work_items,
143 int number_of_slices,
144 int slice_index,
145 long long& first,
146 long long& last) {
147 long long slices = static_cast<long long>(number_of_slices);
148 long long index = static_cast<long long>(slice_index);
149 long long base_size = work_items / slices;
150 long long remainder = work_items % slices;
151 first = index * base_size + std::min(index, remainder);
152 last = first + base_size + (index < remainder ? 1 : 0);
153}
154
155} // namespace threading
156} // namespace execution
157} // namespace q3
int thread_count_for(long long work_items, long long minimum_items_per_thread)
void slice_bounds(long long work_items, int number_of_slices, int slice_index, long long &first, long long &last)
constexpr bool is_compiled_with_openmp()
Definition threading.hpp:40