Stroika Library 3.0d24
 
Loading...
Searching...
No Matches
CPUAffinity.h
Go to the documentation of this file.
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#ifndef _Stroika_Foundation_Execution_CPUAffinity_h_
5#define _Stroika_Foundation_Execution_CPUAffinity_h_ 1
6
7#include "Stroika/Foundation/StroikaPreComp.h"
8
9#include <optional>
10
11#include "Stroika/Foundation/Common/Common.h"
12#include "Stroika/Foundation/Containers/Set.h"
13
14/**
15 * \file
16 * Control which logical CPU cores THIS PROCESS is permitted to run on.
17 *
18 * Companion to Common/SystemConfiguration.h, which DESCRIBES the machine (how many sockets, how many
19 * logical cores, model names). This header does not describe anything - it constrains where the caller
20 * runs. Core numbering here is the same 0-based logical core numbering the OS uses, and the count comes
21 * from there, not from here.
22 *
23 * @see Common::GetNumberOfLogicalCPUCores - how many logical cores exist (cheap, mildly stale)
24 * @see Common::GetSystemConfiguration_CPU - fuller topology, sockets and per-core details
25 *
26 * The motivating use is measurement. A benchmark that migrates between cores mid-run measures the
27 * migration as well as the code: pinning Tests/52 to one core took its run-to-run spread from 8.3%
28 * median (39% worst) to 2.6% (17% worst) - about 3x tighter - while moving the scores themselves not at
29 * all (median ratio 0.992x across 34 tests). So this buys precision, not speed. Pinning ordinary
30 * application code is usually a pessimization, since it denies the scheduler every other core.
31 *
32 * \note PROCESS SCOPE - and that means subtly different things per platform. A real platform
33 * difference, not an implementation gap:
34 * o Windows: SetProcessAffinityMask () is genuinely process-wide - it applies to every
35 * thread, including ones already running.
36 * o Linux: there is no process-wide call. sched_setaffinity (0, ...) sets the CALLING
37 * THREAD. Threads created later inherit the mask; threads already running keep the old
38 * one. Changing those would mean walking /proc/self/task, which this does not do.
39 * For the intended use - pin early, from a still-single-threaded program - the two coincide.
40 *
41 * \note NOT AVAILABLE EVERYWHERE - see kCPUAffinitySupported. macOS has no equivalent of
42 * sched_setaffinity: thread_policy_set (THREAD_AFFINITY_POLICY) is an advisory hint about which
43 * threads want to share a cache, not a pinning primitive, and does nothing whatever on Apple
44 * Silicon. Reported honestly here rather than faked.
45 *
46 * TODO:
47 * @todo Thread::Ptr::SetCPUAffinity () - PER-THREAD affinity, so a caller could pin a thread other
48 * than its own (ThreadPool workers, say). Deliberately not here. It belongs on Thread::Ptr as
49 * a sibling of SetThreadPriority (), which is the established shape for a portable, coarse,
50 * set-only thread knob - including having no getter, which happens to suit affinity too since
51 * Win32 has no GetThreadAffinityMask (and reading it back by calling SetThreadAffinityMask
52 * twice perturbs the thing it claims to report).
53 *
54 * Why wait: SetThreadPriority () copes with being called BEFORE Start () by stashing
55 * fRep_->fInitialPriority_ and applying it at launch, because that is a common sequence.
56 * Affinity has the same usage pattern, so a member that silently did nothing before Start ()
57 * would be a trap manufactured by matching that precedent only halfway - and honoring it
58 * properly needs a new field on the Thread rep plus a hook at thread start. Worth doing when
59 * some caller actually needs to pin another thread; until then this process-scope API covers
60 * the measurement use, and is also where the portable GETTER has to live regardless.
61 *
62 * @todo Windows confines a process to one processor GROUP, so its mask covers at most 64 logical
63 * cores. A >64-core Windows machine needs SetThreadGroupAffinity and a group number threaded
64 * through this API. Not done - no such machine here to test against.
65 */
66
68
69 /**
70 * \brief A set of 0-based logical CPU core numbers, as used by Get/SetCPUAffinity.
71 *
72 * Deliberately a Set<> of core numbers and not a bitmask: it says what it means, it copies by value
73 * like the rest of Stroika, and it does not silently cap at 64 the way a uint64_t would.
74 *
75 * Numbering matches the OS's logical core numbering, so an element is always
76 * < Common::GetNumberOfLogicalCPUCores ().
77 */
79
80 /**
81 * \brief Can CPU affinity be controlled on this platform at all?
82 *
83 * When false: SetCPUAffinity () throws FeatureNotSupportedException, SetCPUAffinityQuietly () returns
84 * false without throwing (the check is if constexpr, so it costs nothing), GetCPUAffinity ()
85 * returns nullopt, and PinToOneLogicalCPUCoreQuietly () returns nullopt having changed nothing.
86 * Test this, or just use a Quietly form - affinity is a tuning knob, and code that *requires* it
87 * cannot run on macOS.
88 */
89 constexpr bool kCPUAffinitySupported =
90#if qStroika_Foundation_Common_Platform_Windows or qStroika_Foundation_Common_Platform_Linux
91 true
92#else
93 false
94#endif
95 ;
96
97 /**
98 * \brief Which logical CPU cores this process may currently run on; nullopt if not knowable.
99 *
100 * This is a LIVE query every time, deliberately unlike Common::GetNumberOfLogicalCPUCores () which
101 * caches with an allowedStaleness. The core count barely changes; an affinity mask can be rewritten
102 * underneath a running process at any moment (a cgroup cpuset being edited, or a launcher applying
103 * taskset), and a stale answer here would be used to pick a core that is no longer permitted.
104 *
105 * Returns nullopt when kCPUAffinitySupported is false, or when the query fails.
106 *
107 * \par Example Usage
108 * \code
109 * // Pin to a core we are actually permitted to use. Never just name core 0: inside
110 * // 'docker run --cpuset-cpus=2,3' it is not in our set, and asking for it fails outright.
111 * if (optional<LogicalCPUCoreSet> allowed = GetCPUAffinity (); allowed and not allowed->empty ()) {
112 * (void)SetCPUAffinityQuietly (LogicalCPUCoreSet{*allowed->Min ()});
113 * }
114 * // ... or just say what you mean, which does exactly that:
115 * (void)PinToOneLogicalCPUCoreQuietly ();
116 * \endcode
117 */
118 optional<LogicalCPUCoreSet> GetCPUAffinity ();
119
120 /**
121 * \brief Restrict this process to the given logical CPU cores.
122 *
123 * \req not cores.empty ()
124 *
125 * \note Deliberately NOT a precondition that each core exists, or that it is currently permitted.
126 * Both are environment rather than program invariant, and both can change between checking
127 * and calling - a cgroup cpuset can be rewritten, cores can be hotplugged - so asserting
128 * would be a race that reports a programmer error for something that is not one. The OS
129 * checks regardless, and reports it through the right channel: an exception here, or false
130 * from SetCPUAffinityQuietly ().
131 *
132 * \note Naming a core outside the currently permitted set fails: the OS will probably not widen a
133 * restriction imposed from outside it (a cgroup cpuset, or a taskset wrapper).
134 *
135 * @see SetCPUAffinityQuietly - usually the one you want, since failing to pin is not an error.
136 */
137 void SetCPUAffinity (const LogicalCPUCoreSet& cores);
138
139 /**
140 * \brief Like SetCPUAffinity (), but returns false rather than throwing.
141 *
142 * False means unsupported on this platform, or the OS refused; either way nothing changed. Prefer
143 * this form: something that would merely *like* to be pinned should not fail because it happens to
144 * be running somewhere that will not allow it.
145 */
146 bool SetCPUAffinityQuietly (const LogicalCPUCoreSet& cores) noexcept;
147
148 /**
149 * \brief Pin to a single logical CPU core chosen from those currently permitted; returns which, or nullopt.
150 *
151 * The measurement helper. It encodes the thing that is easy to get wrong: it chooses from the mask
152 * the caller ALREADY holds rather than naming a core. Hardcoding core 0 is what breaks under
153 * 'docker run --cpuset-cpus=2,3', where taskset reports
154 * "failed to set pid's affinity: Invalid argument" - and a launcher that ignores that failure while
155 * having already redirected its output leaves a zero-byte result file behind.
156 *
157 * Returns nullopt if affinity is unsupported or could not be set, having changed nothing, so the
158 * caller can carry on unpinned - just noisier.
159 */
160 optional<unsigned int> PinToOneLogicalCPUCoreQuietly () noexcept;
161
162}
163
164/*
165 ********************************************************************************
166 ***************************** Implementation Details ***************************
167 ********************************************************************************
168 */
169#include "CPUAffinity.inl"
170
171#endif /*_Stroika_Foundation_Execution_CPUAffinity_h_*/
Set<T> is a container of T, where once an item is added, additionally adds () do nothing.
optional< LogicalCPUCoreSet > GetCPUAffinity()
Which logical CPU cores this process may currently run on; nullopt if not knowable.
constexpr bool kCPUAffinitySupported
Can CPU affinity be controlled on this platform at all?
Definition CPUAffinity.h:89
void SetCPUAffinity(const LogicalCPUCoreSet &cores)
Restrict this process to the given logical CPU cores.
bool SetCPUAffinityQuietly(const LogicalCPUCoreSet &cores) noexcept
Like SetCPUAffinity (), but returns false rather than throwing.
optional< unsigned int > PinToOneLogicalCPUCoreQuietly() noexcept
Pin to a single logical CPU core chosen from those currently permitted; returns which,...