nk
1.0.0
C++ Numerical Computing Runtime
Toggle main menu visibility
Loading...
Searching...
No Matches
parallel_for.hpp
1
// core/include/nk/ops/parallel_for.hpp
2
//
3
// `parallel_for(n, threshold, fn)` — call `fn(start, end)` over a
4
// non-overlapping cover of [0, n).
5
//
6
// * NK_WITH_THREADS undefined → always sequential:
7
// `fn(0, n)` on the caller's thread.
8
// * NK_WITH_THREADS defined AND n ≥ threshold AND the global
9
// thread pool has more than one worker → split [0, n) evenly
10
// across workers, block until all chunks finish.
11
// * Otherwise (n < threshold or single-worker pool) → still
12
// `fn(0, n)` sequentially. Threshold lets per-kernel callers
13
// opt out of threading on small inputs where the hand-off
14
// would dominate.
15
//
16
// SIMD kernels in *_highway.cpp TUs (src/ops/src/ and co-located module backends) use this exclusively
17
// for parallelism. Output buffers must be disjoint per chunk and
18
// inputs read-only — both already true for our element-wise SIMD
19
// kernels.
20
21
#pragma once
22
23
#include <cstddef>
24
25
#if defined(NK_WITH_THREADS)
26
#include <nk/ops/thread_pool.hpp>
27
#endif
28
29
namespace
nk::detail {
30
31
// Suggested per-kernel parallel thresholds. Chosen so the per-call
32
// overhead of the pool dispatch (~ a few µs of mutex + condvar on
33
// native, ~30-100 µs per Web Worker dispatch on WASM) is well under
34
// the kernel's expected wall time at that N. Tune later if profiling
35
// tells us otherwise.
36
//
37
// `kCheapElementwiseThreshold` is for the cheapest memory-bound ops
38
// (abs / + / - / .* / ./). Each element costs essentially one SIMD
39
// instruction; the kernel saturates DRAM bandwidth at modest N and
40
// parallelism doesn't help past ~6 workers.
41
//
42
// The threshold is platform-dependent because pool dispatch latency
43
// is wildly different:
44
// * native std::thread + condvar: a few µs per dispatch
45
// * Emscripten pthread (Web Workers + SharedArrayBuffer atomics):
46
// ~30-100 µs per Worker signal × 6 Workers ≈ 200-600 µs per
47
// parallel_for call.
48
// At 200+ µs of dispatch overhead per call, the abs kernel at N=1M
49
// (~0.5 ms single-thread) gets slower with threading. Raising the
50
// WASM threshold to 4 M keeps practical-size calls on the caller's
51
// thread there.
52
//
53
// `kTranscendentalThreshold` is much smaller because sin/cos/exp/log
54
// have high arithmetic intensity per element (Highway's polynomial
55
// approximations) — the kernel pays for the dispatch even on small N.
56
inline
constexpr
std::size_t kElementwiseThreshold = 16 * 1024;
57
#if defined(__EMSCRIPTEN__)
58
inline
constexpr
std::size_t kCheapElementwiseThreshold = 4 * 1024 * 1024;
59
#else
60
inline
constexpr
std::size_t kCheapElementwiseThreshold = 256 * 1024;
61
#endif
62
inline
constexpr
std::size_t kTranscendentalThreshold = 4 * 1024;
63
64
// Below this N the cheapest element-wise ops (+ - .* ./) skip the Highway
65
// dynamic-dispatch path and run an inline scalar loop the compiler
66
// auto-vectorizes. On native (MSVC) the indirect HWY_DYNAMIC_DISPATCH call
67
// into the non-inlinable per-target body loses to inline autovec at
68
// cache-resident sizes — up to ~8x slower (see
69
// bugs/ops/cheap-elementwise-simd-small-n). On WASM the dispatch is
70
// static/inlinable and already wins on small inputs, so the gate is disabled
71
// (0 → `n < 0` never trips). Set at the measured native break-even (~256K).
72
#if defined(__EMSCRIPTEN__)
73
inline
constexpr
std::size_t kSimdInlineThreshold = 0;
74
#else
75
inline
constexpr
std::size_t kSimdInlineThreshold = 256 * 1024;
76
#endif
77
78
// Worker caps for memory-bandwidth-bound kernels. On dual-channel
79
// DDR5-6400 (≈ 100 GB/s aggregate), 4-6 workers already saturate
80
// the bus on `z = x op y` and friends; more workers add signaling
81
// noise without helping throughput. Compute-bound kernels (mtimes,
82
// transcendentals) don't need a cap and benefit from every core.
83
inline
constexpr
int
kElementwiseMaxWorkers = 6;
84
85
template
<
typename
F>
86
inline
void
parallel_for(std::size_t n, std::size_t threshold, F &&fn,
87
int
max_workers = 0)
88
{
89
#if defined(NK_WITH_THREADS)
90
auto
&pool = ThreadPool::global();
91
if
(n >= threshold && pool.workers() > 1) {
92
pool.run(n, std::forward<F>(fn), max_workers);
93
return
;
94
}
95
#else
96
(void)threshold;
97
(void)max_workers;
98
#endif
99
if
(n > 0)
100
fn(
static_cast<
std::size_t
>
(0), n);
101
}
102
103
}
// namespace nk::detail
src
ops
include
nk
ops
parallel_for.hpp
Generated by
1.17.0