GCC Code Coverage Report


Directory: ./
File: lib/geogram/basic/thread_sync.h
Date: 2026-09-07 02:36:43
Exec Total Coverage
Lines: 45 45 100.0%
Functions: 10 10 100.0%
Branches: 15 28 53.6%

Line Branch Exec Source
1 /*
2 * Copyright (c) 2000-2022 Inria
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * * Redistributions of source code must retain the above copyright notice,
9 * this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above copyright notice,
11 * this list of conditions and the following disclaimer in the documentation
12 * and/or other materials provided with the distribution.
13 * * Neither the name of the ALICE Project-Team nor the names of its
14 * contributors may be used to endorse or promote products derived from this
15 * software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 * POSSIBILITY OF SUCH DAMAGE.
28 *
29 * Contact: Bruno Levy
30 *
31 * https://www.inria.fr/fr/bruno-levy
32 *
33 * Inria,
34 * Domaine de Voluceau,
35 * 78150 Le Chesnay - Rocquencourt
36 * FRANCE
37 *
38 */
39
40 #ifndef GEOGRAM_BASIC_THREAD_SYNC
41 #define GEOGRAM_BASIC_THREAD_SYNC
42
43 /**
44 * \file geogram/basic/thread_sync.h
45 * \brief Functions and classes for process manipulation
46 */
47
48 #include <geogram/basic/common.h>
49 #include <geogram/basic/numeric.h>
50 #include <geogram/basic/assert.h>
51 #include <geogram/basic/argused.h>
52 #include <vector>
53 #include <atomic>
54
55 // On Windows/MSCV, we need to use a special implementation
56 // of spinlocks because std::atomic_flag in MSVC's stl does
57 // not fully implement the norm (lacks a constructor).
58 #ifdef GEO_OS_WINDOWS
59 #include <windows.h>
60 #include <intrin.h>
61 #pragma intrinsic(_InterlockedCompareExchange16)
62 #pragma intrinsic(_WriteBarrier)
63 #endif
64
65 // On MacOS, I get many warnings with atomic_flag initialization,
66 // such as std::atomic_flag f = ATOMIC_FLAG_INIT
67 #if defined(__clang__)
68 #pragma GCC diagnostic ignored "-Wbraced-scalar-init"
69 #endif
70
71 /**
72 * \brief executes the pause instruction
73 * \details should be called when a spinlock is spinning
74 */
75 122530318 inline void geo_pause() {
76 #ifdef GEO_OS_WINDOWS
77 YieldProcessor();
78 #else
79 # ifdef GEO_PROCESSOR_X86
80 # ifdef __ICC
81 _mm_pause();
82 # else
83 122530318 __builtin_ia32_pause();
84 # endif
85 # endif
86 #endif
87 122530318 }
88
89 /*******************************************************************************/
90
91 #ifdef GEO_OS_WINDOWS
92
93 // Windows-specific spinlock implementation.
94 // I'd have prefered to use std::atomic_flag for everybody,
95 // unfortunately atomic_flag's constructor is not implemented in MSCV's stl,
96 // so we reimplement them using atomic compare-exchange functions...
97
98 namespace GEO {
99 namespace Process {
100 /** A lightweight synchronization structure. */
101 typedef short spinlock;
102
103 /** The initialization value of a spin lock. */
104 # define GEOGRAM_SPINLOCK_INIT 0
105 inline void acquire_spinlock(volatile spinlock& x) {
106 while(_InterlockedCompareExchange16(&x, 1, 0) == 1) {
107 // Intel recommends to have a PAUSE asm instruction
108 // in the spinlock loop. Under MSVC/Windows,
109 // YieldProcessor() is a macro that calls the
110 // (undocumented) _mm_pause() intrinsic function
111 // that generates a PAUSE opcode.
112 YieldProcessor();
113 }
114 // We do not need _ReadBarrier() here since
115 // _InterlockedCompareExchange16
116 // "acts as a full barrier in VC2005" according to the doc
117 }
118
119 inline void release_spinlock(volatile spinlock& x) {
120 _WriteBarrier(); // prevents compiler reordering
121 x = 0;
122 }
123
124 }
125 }
126
127 /*******************************************************************************/
128
129 #else
130
131 namespace GEO {
132 namespace Process {
133
134 /** The initialization value of a spinlock. */
135 // Note: C++20 does not need it anymore, in C++20
136 // std::atomic_flag's constructor initializes it,
137 // we keep it because
138 // - we are using C++17
139 // - the Windows implementation that uses integers rather than
140 // std::atomic_flag needs an initialization value.
141 #define GEOGRAM_SPINLOCK_INIT ATOMIC_FLAG_INIT
142
143 /**
144 * \brief A lightweight synchronization structure.
145 * \details See
146 * - https://rigtorp.se/spinlock/
147 * - https://www.sobyte.net/post/2022-06/cpp-memory-order/
148 */
149 typedef std::atomic_flag spinlock;
150
151 /**
152 * \brief Loops until \p x is available then reserves it.
153 * \param[in] x a spinlock
154 */
155 2121837 inline void acquire_spinlock(volatile spinlock& x) {
156 for (;;) {
157
2/2
✓ Branch 0 taken 2121837 times.
✓ Branch 1 taken 122530150 times.
247182137 if (!x.test_and_set(std::memory_order_acquire)) {
158 2121837 break;
159 }
160 // If compiling in C++20 we can be slightly more efficient when spinning
161 // (avoid unrequired atomic operations, just "peek" the flag)
162 #if defined(__cpp_lib_atomic_flag_test)
163 while (x.test(std::memory_order_relaxed))
164 #endif
165 122530150 geo_pause();
166 }
167 2121837 }
168
169 /**
170 * \brief Makes \p x available to other threads.
171 * \param[in] x a spinlock
172 */
173 2121837 inline void release_spinlock(volatile spinlock& x) {
174 x.clear(std::memory_order_release);
175 2121837 }
176
177 }
178 }
179 #endif
180
181 /****************************************************************************/
182
183 namespace GEO {
184 namespace Process {
185
186 /**
187 * \brief An array of light-weight synchronisation
188 * primitives (spinlocks).
189 *
190 * \details This is the reference implementation, that uses
191 * an array of spinlock. There is also a more memory-efficient
192 * implementation, CompactSpinLockArray, to be used for a very
193 * large number of spinlocks.
194 *
195 * \see acquire_spinlock(), release_spinlock()
196 */
197 class BasicSpinLockArray {
198 public:
199 /**
200 * \brief Constructs a new BasicSpinLockArray of size 0.
201 */
202 BasicSpinLockArray() : spinlocks_(nullptr), size_(0) {
203 }
204
205 /**
206 * \brief Constructs a new BasicSpinLockArray of size \p size_in.
207 * \param[in] size_in number of spinlocks in the array.
208 */
209 BasicSpinLockArray(index_t size_in) : spinlocks_(nullptr), size_(0) {
210 resize(size_in);
211 }
212
213 /**
214 * \brief Forbids copy
215 */
216 BasicSpinLockArray(const BasicSpinLockArray& rhs) = delete;
217
218 /**
219 * \brief Forbids copy
220 */
221 BasicSpinLockArray& operator=(
222 const BasicSpinLockArray& rhs
223 ) = delete;
224
225 /**
226 * \brief Resizes a BasicSpinLockArray.
227 * \details All the spinlocks are reset to 0.
228 * \param[in] size_in The desired new size.
229 */
230 void resize(index_t size_in) {
231 delete[] spinlocks_;
232 spinlocks_ = new spinlock[size_in];
233 size_ = size_in;
234 // Need to initialize the spinlocks to false (dirty !)
235 // (maybe use placement new on each item..., to be tested)
236 for(index_t i=0; i<size_; ++i) {
237 Process::release_spinlock(spinlocks_[i]);
238 }
239 }
240
241 /**
242 * \brief Resets size to 0 and clears all the memory.
243 */
244 void clear() {
245 delete[] spinlocks_;
246 spinlocks_ = nullptr;
247 }
248
249 /**
250 * \brief Gets the number of spinlocks in this array.
251 */
252 index_t size() const {
253 return size_;
254 }
255
256 /**
257 * \brief Acquires a spinlock at a given index
258 * \details Loops until spinlock at index \p i is available then
259 * reserve it.
260 * \param[in] i index of the spinlock
261 */
262 void acquire_spinlock(index_t i) {
263 geo_debug_assert(i < size());
264 GEO::Process::acquire_spinlock(spinlocks_[i]);
265 }
266
267 /**
268 * \brief Releases a spinlock at a given index
269 * \details Makes spinlock at index \p i available to other threads.
270 * \param[in] i index of the spinlock
271 */
272 void release_spinlock(index_t i) {
273 geo_debug_assert(i < size());
274 GEO::Process::release_spinlock(spinlocks_[i]);
275 }
276
277 private:
278 // Cannot use a std::vector because std::atomic_flag does not
279 // have copy ctor nor assignment operator.
280 spinlock* spinlocks_;
281 index_t size_;
282 };
283 }
284 }
285
286 /*******************************************************************************/
287
288 namespace GEO {
289 namespace Process {
290
291 /**
292 * \brief An array of light-weight synchronisation
293 * primitives (spinlocks).
294 *
295 * \details In this implementation, storage is optimized so that
296 * a single bit per spinlock is used. This implementation uses
297 * std::atomic<uint32_t>
298 *
299 * \see acquire_spinlock(), release_spinlock()
300 */
301 class CompactSpinLockArray {
302 public:
303 /**
304 * \brief Constructs a new SpinLockArray of size 0.
305 */
306 161 CompactSpinLockArray() : spinlocks_(nullptr), size_(0) {
307 161 }
308
309 /**
310 * \brief Constructs a new CompactSpinLockArray of size \p size_in.
311 * \param[in] size_in number of spinlocks in the array.
312 */
313 CompactSpinLockArray(index_t size_in) : spinlocks_(nullptr),size_(0){
314 resize(size_in);
315 }
316
317 /**
318 * \brief CompactSpinLockArray destructor
319 */
320 161 ~CompactSpinLockArray() {
321 161 clear();
322 161 }
323
324 /**
325 * \brief Forbids copy
326 */
327 CompactSpinLockArray(const CompactSpinLockArray& rhs) = delete;
328
329 /**
330 * \brief Forbids copy
331 */
332 CompactSpinLockArray& operator=(
333 const CompactSpinLockArray& rhs
334 ) = delete;
335
336 /**
337 * \brief Resizes a CompactSpinLockArray.
338 * \details All the spinlocks are reset to 0.
339 * \param[in] size_in The desired new size.
340 */
341 531 void resize(index_t size_in) {
342
2/2
✓ Branch 0 taken 75 times.
✓ Branch 1 taken 456 times.
531 if(size_ != size_in) {
343 75 size_ = size_in;
344 75 index_t nb_words = (size_ >> 5) + 1;
345
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 75 times.
75 delete[] spinlocks_;
346 // Each entry is initialized with 0 -------------v
347
4/6
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
✓ Branch 4 taken 3181 times.
✓ Branch 5 taken 75 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 75 times.
3256 spinlocks_ = new std::atomic<uint32_t>[nb_words]{};
348 }
349
350 // Test at compile time that we are using atomic
351 // uint32_t operations (and not using an additional
352 // lock which would be catastrophic in terms of
353 // performance)
354 static_assert(std::atomic<uint32_t>::is_always_lock_free);
355 531 }
356
357 /**
358 * \brief Gets the number of spinlocks in this array.
359 */
360 36217314 index_t size() const {
361 36217314 return size_;
362 }
363
364 /**
365 * \brief Resets size to 0 and clears all the memory.
366 */
367 161 void clear() {
368
2/2
✓ Branch 0 taken 75 times.
✓ Branch 1 taken 86 times.
161 delete[] spinlocks_;
369 161 size_ = 0;
370 161 }
371
372 /**
373 * \brief Acquires a spinlock at a given index
374 * \details Loops until spinlock at index \p i is available then
375 * reserve it.
376 * \param[in] i index of the spinlock
377 */
378 18108657 void acquire_spinlock(index_t i) {
379
1/6
✗ Branch 1 not taken.
✓ Branch 2 taken 18108657 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
18108657 geo_debug_assert(i < size());
380 18108657 index_t w = i >> 5;
381 18108657 uint32_t b = uint32_t(i & 31);
382 18108657 uint32_t mask = (1u << b);
383 18108657 while(
384 18108825 (spinlocks_[w].fetch_or(
385 mask, std::memory_order_acquire
386
2/2
✓ Branch 0 taken 168 times.
✓ Branch 1 taken 18108657 times.
18108825 ) & mask) != 0
387 ) {
388 168 geo_pause();
389 }
390 18108657 }
391
392 /**
393 * \brief Releases a spinlock at a given index
394 * \details Makes spinlock at index \p i available to other threads.
395 * \param[in] i index of the spinlock
396 */
397 18108657 void release_spinlock(index_t i) {
398
1/6
✗ Branch 1 not taken.
✓ Branch 2 taken 18108657 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
18108657 geo_debug_assert(i < size());
399 18108657 index_t w = i >> 5;
400 18108657 uint32_t b = uint32_t(i & 31);
401 18108657 uint32_t mask = ~(1u << b);
402 18108657 spinlocks_[w].fetch_and(mask, std::memory_order_release);
403 18108657 }
404
405 private:
406 // Cannot use a std::vector because std::atomic<> does not
407 // have copy ctor nor assignment operator.
408 std::atomic<uint32_t>* spinlocks_;
409 index_t size_;
410 };
411
412 }
413 }
414
415 /*******************************************************************************/
416
417 namespace GEO {
418 namespace Process {
419 typedef CompactSpinLockArray SpinLockArray;
420 }
421 }
422
423 /*******************************************************************************/
424
425 #endif
426