Stroika Library 3.0d24
 
Loading...
Searching...
No Matches
CPUAffinity.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
6#if qStroika_Foundation_Common_Platform_Windows
7#include <Windows.h>
8#elif qStroika_Foundation_Common_Platform_Linux
9#include <sched.h>
10#endif
11
13#include "Stroika/Foundation/Execution/Exceptions.h"
14#include "Stroika/Foundation/Execution/FeatureNotSupportedException.h"
15
16#include "CPUAffinity.h"
17
18using namespace Stroika::Foundation;
19using namespace Stroika::Foundation::Common;
20using namespace Stroika::Foundation::Execution;
21
22namespace {
23 /*
24 * Deliberately OUTSIDE the platform #if blocks, though only the unsupported branch calls it. A body
25 * inside '#else' is compiled on no machine this is developed on - so a typo there would first appear
26 * on macOS, which is the platform least able to check it. Declaring it unconditionally means every
27 * compiler sees it.
28 */
29 [[noreturn]] void ThrowCPUAffinityUnsupported_ ()
30 {
31 Throw (FeatureNotSupportedException{"CPU affinity"sv});
32 }
33
34 /*
35 * The largest core number this platform's mask TYPE can represent. Not a property of the machine - an
36 * API limit, and a wildly inconsistent one: a Windows DWORD_PTR mask covers one processor group, so 32
37 * in a 32-bit build and 64 in a 64-bit one, while glibc's cpu_set_t holds CPU_SETSIZE (1024).
38 */
39 constexpr unsigned int kMaxRepresentableCore_ =
40#if qStroika_Foundation_Common_Platform_Windows
41 static_cast<unsigned int> (sizeof (DWORD_PTR) * 8)
42#elif qStroika_Foundation_Common_Platform_Linux
43 static_cast<unsigned int> (CPU_SETSIZE)
44#else
45 0
46#endif
47 ;
48}
49
50namespace {
51#if qStroika_Foundation_Common_Platform_Windows
52 /*
53 * Windows affinity is a DWORD_PTR bitmask over the process's single processor GROUP, so it tops out at
54 * 64 logical cores (32 in a 32-bit build). Beyond that needs SetThreadGroupAffinity and a group
55 * number, which this API does not model - see the @todo in CPUAffinity.h.
56 */
57 constexpr unsigned int kMaxCoreInAMask_ = static_cast<unsigned int> (sizeof (DWORD_PTR) * 8);
58
59 DWORD_PTR mkMask_ (const LogicalCPUCoreSet& cores)
60 {
61 DWORD_PTR mask{};
62 for (unsigned int c : cores) {
63 Require (c < kMaxCoreInAMask_); // else silently unrepresentable, which would be worse than failing
64 mask |= (static_cast<DWORD_PTR> (1) << c);
65 }
66 return mask;
67 }
68 LogicalCPUCoreSet mkSet_ (DWORD_PTR mask)
69 {
71 for (unsigned int c = 0; c < kMaxCoreInAMask_; ++c) {
72 if (mask & (static_cast<DWORD_PTR> (1) << c)) {
73 cores.Add (c);
74 }
75 }
76 return cores;
77 }
78#elif qStroika_Foundation_Common_Platform_Linux
79 cpu_set_t mkMask_ (const LogicalCPUCoreSet& cores)
80 {
81 cpu_set_t cpuSet;
82 CPU_ZERO (&cpuSet);
83 for (unsigned int c : cores) {
84 Require (c < CPU_SETSIZE);
85 CPU_SET (c, &cpuSet);
86 }
87 return cpuSet;
88 }
89 LogicalCPUCoreSet mkSet_ (const cpu_set_t& cpuSet)
90 {
92 for (unsigned int c = 0; c < CPU_SETSIZE; ++c) {
93 if (CPU_ISSET (c, &cpuSet)) {
94 cores.Add (c);
95 }
96 }
97 return cores;
98 }
99#endif
100}
101
102/*
103 ********************************************************************************
104 ********************** Execution::GetCPUAffinity *******************************
105 ********************************************************************************
106 */
107optional<LogicalCPUCoreSet> Execution::GetCPUAffinity ()
108{
109#if qStroika_Foundation_Common_Platform_Windows
110 DWORD_PTR processMask{};
111 DWORD_PTR systemMask{};
112 if (::GetProcessAffinityMask (::GetCurrentProcess (), &processMask, &systemMask) == 0) {
113 return nullopt;
114 }
115 return mkSet_ (processMask);
116#elif qStroika_Foundation_Common_Platform_Linux
117 // pid 0 == the calling thread. See the PROCESS SCOPE note in CPUAffinity.h for why that is the right
118 // answer here even though it is not literally process-wide on Linux.
119 cpu_set_t cpuSet;
120 CPU_ZERO (&cpuSet);
121 if (::sched_getaffinity (0, sizeof (cpuSet), &cpuSet) != 0) {
122 return nullopt;
123 }
124 return mkSet_ (cpuSet);
125#else
126 return nullopt; // macOS and anything else: no pinning primitive, so nothing to report
127#endif
128}
129
130/*
131 ********************************************************************************
132 ********************** Execution::SetCPUAffinity *******************************
133 ********************************************************************************
134 */
135void Execution::SetCPUAffinity ([[maybe_unused]] const LogicalCPUCoreSet& cores)
136{
137 Require (not cores.empty ());
138 // NB: deliberately NO Require () that each core is < GetNumberOfLogicalCPUCores (), or that it is in
139 // the currently permitted set. Both are environment, not program invariant, and both can change
140 // between the check and the call - a cgroup cpuset can be rewritten, cores can be hotplugged. Checking
141 // would be a race that reports a programmer error for something that is not one. The OS checks anyway,
142 // and reports it as an exception (or false, from the Quietly form), which is the right channel for it.
143 //
144 // A core number too large for the platform's MASK is a throw for the same reason, and this one is
145 // learned the hard way: it started as a Require () inside the mask builders, which meant a release
146 // build returned false while a debug build ABORTED on the identical call. Nothing a caller can know
147 // portably either - the ceiling is 32, 64 or 1024 depending on platform and word size - so it belongs
148 // in the same channel as any other refusal rather than in an assertion.
149 if (kCPUAffinitySupported and not cores.All ([] (unsigned int c) { return c < kMaxRepresentableCore_; })) {
150 Throw (RuntimeErrorException{"CPU core number is too large for this platform's affinity mask"sv});
151 }
152#if qStroika_Foundation_Common_Platform_Windows
153 if (::SetProcessAffinityMask (::GetCurrentProcess (), mkMask_ (cores)) == 0) {
154 ThrowSystemErrNo ();
155 }
156#elif qStroika_Foundation_Common_Platform_Linux
157 cpu_set_t cpuSet = mkMask_ (cores);
158 ThrowPOSIXErrNoIfNegative (::sched_setaffinity (0, sizeof (cpuSet), &cpuSet));
159#else
160 // NB: a throw, not AssertNotImplemented () - that compiles out in a release build, which would leave
161 // this returning normally having done nothing, while the header promises it throws. kCPUAffinitySupported
162 // is false here, so callers who do not want the throw should use SetCPUAffinityQuietly ().
163 ThrowCPUAffinityUnsupported_ ();
164#endif
165}
166
167/*
168 ********************************************************************************
169 *************** Execution::PinToOneLogicalCPUCoreQuietly ***********************
170 ********************************************************************************
171 */
172optional<unsigned int> Execution::PinToOneLogicalCPUCoreQuietly () noexcept
173{
174 if constexpr (not kCPUAffinitySupported) {
175 return nullopt;
176 }
177 else {
178 try {
179 // Ask what we HOLD and choose from that. Naming a core outright is the bug this exists to
180 // avoid: under 'docker run --cpuset-cpus=2,3' core 0 is not ours and the call fails outright.
181 optional<LogicalCPUCoreSet> allowed = GetCPUAffinity ();
182 if (not allowed or allowed->empty ()) {
183 return nullopt;
184 }
185 unsigned int pinTo = *allowed->Min ();
187 return pinTo;
188 }
189 return nullopt;
190 }
191 catch (...) {
192 return nullopt; // noexcept, and failing to pin is never worth propagating
193 }
194 }
195}
Set<T> is a container of T, where once an item is added, additionally adds () do nothing.
nonvirtual void Add(ArgByValueType< value_type > item)
Definition Set.inl:138
optional< LogicalCPUCoreSet > GetCPUAffinity()
Which logical CPU cores this process may currently run on; nullopt if not knowable.
void Throw(T &&e2Throw)
identical to builtin C++ 'throw' except that it does helpful, type dependent DbgTrace() messages firs...
Definition Throw.inl:43
constexpr bool kCPUAffinitySupported
Can CPU affinity be controlled on this platform at all?
Definition CPUAffinity.h:89
bool SetCPUAffinityQuietly(const LogicalCPUCoreSet &cores) noexcept
Like SetCPUAffinity (), but returns false rather than throwing.
INT_TYPE ThrowPOSIXErrNoIfNegative(INT_TYPE returnCode)