CX Framework
Cross-platform C utility framework
Loading...
Searching...
No Matches
aspin.h
1#pragma once
2
3#include <cx/math/lcg.h>
4#include <cx/platform/cpu.h>
5#include <cx/platform/os.h>
6#include <cx/thread/atomic.h>
7#include <cx/time/clock.h>
8#include <cx/time/time.h>
9#include <cx/utils/compare.h>
10
11CX_C_BEGIN
12
13// Common functions for adaptive spin threading primitives
14
15#define ASPIN_MAX_USEC 10 // hard cap in microseconds
16#define ASPIN_INITIAL_TARGET 10 // initial number of cycles to spin
17#define ASPIN_NOSPIN (-2147483647 - 1)
18
19#if DEBUG_LEVEL >= 2
20#define ASPIN_PERF_STATS
21#endif
22
23typedef struct AdaptiveSpin {
24 atomic(int32) spintarget;
25#ifdef ASPIN_PERF_STATS
26 atomic(intptr) stats_uncontended; // uncontended acquisition of primitive
27 atomic(intptr) stats_spin; // times primitive was acquired after spinning
28 atomic(intptr) stats_futex; // times primitive was acquired on non-spin (futex) path
29 atomic(intptr) stats_capped; // times spin loop was capped by ASPIN_MAX_USEC
30 atomic(intptr) stats_timeout; // times the primitive failed due to timeout
31 atomic(intptr) stats_yield; // times the primitive yielded CPU to reduce contention
32#endif
33} AdaptiveSpin;
34
35typedef struct AdaptiveSpinState {
36 int64 now;
37 int64 start;
38 int64 endtime;
39 int64 spincap;
40 int32 curtarget;
41 int32 spincount;
42 int32 contention;
43 uint32 rstate;
44} AdaptiveSpinState;
45
46_meta_inline void aspinRecordUncontended(_Inout_ AdaptiveSpin* aspin)
47{
48#ifdef ASPIN_PERF_STATS
49 atomicFetchAdd(intptr, &aspin->stats_uncontended, 1, Relaxed);
50#else
51 (void)aspin;
52#endif
53}
54
55_meta_inline void aspinRecordSpin(_Inout_ AdaptiveSpin* aspin)
56{
57#ifdef ASPIN_PERF_STATS
58 atomicFetchAdd(intptr, &aspin->stats_spin, 1, Relaxed);
59#else
60 (void)aspin;
61#endif
62}
63
64_meta_inline void aspinRecordFutex(_Inout_ AdaptiveSpin* aspin)
65{
66#ifdef ASPIN_PERF_STATS
67 atomicFetchAdd(intptr, &aspin->stats_futex, 1, Relaxed);
68#else
69 (void)aspin;
70#endif
71}
72
73_meta_inline void aspinRecordCapped(_Inout_ AdaptiveSpin* aspin)
74{
75#ifdef ASPIN_PERF_STATS
76 atomicFetchAdd(intptr, &aspin->stats_capped, 1, Relaxed);
77#else
78 (void)aspin;
79#endif
80}
81
82_meta_inline void aspinRecordTimeout(_Inout_ AdaptiveSpin* aspin)
83{
84#ifdef ASPIN_PERF_STATS
85 atomicFetchAdd(intptr, &aspin->stats_timeout, 1, Relaxed);
86#else
87 (void)aspin;
88#endif
89}
90
91_meta_inline void aspinRecordYield(_Inout_ AdaptiveSpin* aspin)
92{
93#ifdef ASPIN_PERF_STATS
94 atomicFetchAdd(intptr, &aspin->stats_yield, 1, Relaxed);
95#else
96 (void)aspin;
97#endif
98}
99
100_meta_inline void aspinInit(_Out_ AdaptiveSpin* aspin, bool nospin)
101{
102 memset(aspin, 0, sizeof(AdaptiveSpin));
103
104 // never spin if there's only a single core, just a waste of CPU
105 if (osPhysicalCPUs() == 1)
106 nospin = true;
107
108 atomicStore(int32, &aspin->spintarget, nospin ? ASPIN_NOSPIN : ASPIN_INITIAL_TARGET, Relaxed);
109}
110
111_meta_inline void aspinBegin(_Inout_ AdaptiveSpin* aspin, _Out_ AdaptiveSpinState* ass,
112 int64 timeout)
113{
114 ass->now = clockTimer();
115 ass->start = ass->now;
116 ass->spincap = ass->start + ASPIN_MAX_USEC;
117 ass->endtime = (timeout == timeForever) ? timeForever : ass->start + timeout;
118 ass->curtarget = atomicLoad(int32, &aspin->spintarget, Relaxed);
119 if (ass->curtarget < 1 && ass->curtarget != ASPIN_NOSPIN)
120 ass->curtarget = ASPIN_INITIAL_TARGET;
121 ass->spincount = (ass->curtarget == ASPIN_NOSPIN) ? 0 : ass->curtarget * 2; // -1 == nospin
122 ass->contention = 0;
123 ass->rstate = (ass->now & 0xffffffff);
124}
125
126_meta_inline bool aspinSpin(_Inout_ AdaptiveSpin* aspin, _Inout_ AdaptiveSpinState* ass)
127{
128 // clockTimer may be an expensive system call on some platforms.
129 // Only update clock once every 8 loops, for a tighter spin and less latency.
130 if ((ass->spincount & 7) == 7)
131 ass->now = clockTimer();
132
133 // check if we hit the hard cap on spin time
134 if (ass->spincount > 0 && ass->now > ass->spincap) {
135 // this sets the target to half the current spincount, since the cap will be double that
136 atomicStore(int32, &aspin->spintarget, (ass->curtarget * 2 - ass->spincount) / 2, Relaxed);
137 ass->spincount = 0;
138 aspinRecordCapped(aspin);
139 }
140
141 if (ass->spincount > 0) {
142 --ass->spincount;
143 _CPU_PAUSE;
144 return true;
145 }
146
147 return false;
148}
149
150_meta_inline bool aspinTimeout(_Inout_ AdaptiveSpin* aspin, _Inout_ AdaptiveSpinState* ass)
151{
152 // early out if we don't have a timeout -- skip clock update
153 if (ass->endtime == timeForever)
154 return false;
155
156 // if spinloop is done, update clock here instead since the futex wait
157 // could be a very long time
158 if (ass->spincount == 0)
159 ass->now = clockTimer();
160
161 if (ass->now > ass->endtime) {
162 aspinRecordTimeout(aspin);
163 return true;
164 }
165 return false;
166}
167
168_meta_inline void aspinAdapt(_Inout_ AdaptiveSpin* aspin, _Inout_ AdaptiveSpinState* ass)
169{
170 // don't adapt if we timed out entirely
171 if (ass->now > ass->endtime)
172 return;
173
174 if (ass->curtarget != ASPIN_NOSPIN) {
175 if (ass->spincount > 0) {
176 // adjust adaptive target based on how much spinning we did
177 int32 realtarget = atomicLoad(int32, &aspin->spintarget, Relaxed);
178 atomicFetchAdd(int32,
179 &aspin->spintarget,
180 clamp(ass->curtarget - ass->spincount, -realtarget, realtarget) / 8 + 1,
181 Relaxed);
182 aspinRecordSpin(aspin);
183 } else {
184 if (ass->now <= ass->spincap) {
185 // we had to go to futuxes, give it a boost
186 atomicFetchAdd(int32, &aspin->spintarget, ass->curtarget / 8 + 1, Relaxed);
187 }
188 aspinRecordFutex(aspin);
189 }
190 } else {
191 aspinRecordFutex(aspin);
192 }
193}
194
195_meta_inline int64 aspinTimeoutRemaining(_In_ AdaptiveSpinState* ass)
196{
197 return (ass->endtime == timeForever) ? timeForever : ass->endtime - ass->now;
198}
199
200// call this function when there is contention on a CAS
201_meta_inline void
202aspinHandleContention(_Inout_opt_ AdaptiveSpin* aspin, _Inout_ AdaptiveSpinState* ass)
203{
204 // This algorithm is cruicial to maintaining high performance even under extreme contention.
205 // Earlier versions of these primitives started to suffer degradation when many concurrent
206 // threads attempted to get the mutex/etc simultaneously. They would spend a lot of time
207 // spinning on a CAS on the underlying atomic, attempting to get into a state where they
208 // could wait with a futex/semaphore.
209
210 // By calling this function after a failed CAS, a count of failures is maintained. As the
211 // count increases, so does the probability that the thread will yield its quantum, reducing
212 // contention enough for another thread to succeed on the CAS and break the cycle.
213
214 // We use a poor quality but fast LCG pseudorandom number generator that is good enough
215 // for this purpose.
216
217 if (lcgRandom(&ass->rstate) % (++ass->contention + 7) > 8) {
218 if (aspin)
219 aspinRecordYield(aspin);
220 osYield();
221 } else {
222 for (int i = ass->contention * ass->contention; i >= 0; --i) {
223 _CPU_PAUSE;
224 }
225 }
226}
227
228_meta_inline void aspinEndContention(_Inout_ AdaptiveSpinState* ass)
229{
230 ass->contention = 0;
231}
232
233CX_C_END
Atomic operations.
System clock functions.
Comparison and clamping macros.
CPU-specific operations and atomic primitives.
int32 lcgRandom(uint32 *state)
Definition lcg.h:70
int osPhysicalCPUs()
void osYield()
int64 clockTimer()
#define timeForever
Maximum representable time value (approximately year 294,276 CE)
Definition time.h:15
Simple linear congruential pseudo-random number generator.
Operating system services.
Time manipulation and conversion functions.