GCC Code Coverage Report


src/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
80 / 80
100.0%
Branches:
13 / 14, 18 excl
92.9%
Functions:
12 / 12, 1 excl
100.0%

util/concurrency.spice
Line Branch Exec Source
1 // Copyright (c) 2021-2026 ChilliBits. All rights reserved.
2
3 // Std imports
4 import "std/os/recursive-mutex";
5 import "std/os/thread-pool";
6 import "std/data/unordered-map";
7
8 /**
9 * Global switch that tells the process-wide caches of the compiler (type registry, function lookup cache, ...) whether
10 * compiler passes are currently executed on more than one thread.
11 *
12 * Those caches sit on the hottest paths of the single-threaded front end and middle end, where taking a lock for every
13 * access would be pure overhead. The back end on the other hand runs one pipeline per source file in parallel (see
14 * SourceFile.runBackEnd), and there the caches do need protection. ParallelSection flips this switch for exactly the
15 * time span in which worker threads are alive. It is only written while no worker thread is alive, so the creation and
16 * the joining of the worker threads order the accesses to it.
17 */
18 bool CONCURRENT_PASSES_RUNNING = false;
19
20 /**
21 * RAII marker for a section of the compiler in which passes are executed on multiple threads.
22 *
23 * It has to be entered before the worker threads are handed any work and left only after all of them are joined again,
24 * so that every ConditionalLock taken on a worker thread actually locks.
25 */
26 public type ParallelSection struct {
27 3 bool active = false
28 }
29
30 3 public p ParallelSection.ctor() {
31 3 this.active = true;
32 3 CONCURRENT_PASSES_RUNNING = true;
33 3 }
34
35 3 public p ParallelSection.dtor() {
36
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if this.active {
37 3 CONCURRENT_PASSES_RUNNING = false;
38 3 }
39 3 }
40
41 /**
42 * Check if compiler passes are currently executed on more than one thread
43 *
44 * @return Concurrent passes running or not
45 */
46 7769752 public f<bool> areConcurrentPassesRunning() {
47 7769752 return CONCURRENT_PASSES_RUNNING;
48 }
49
50 /**
51 * RAII lock that is only actually acquired while compiler passes run concurrently.
52 *
53 * It uses the std RecursiveMutex for the symbol registry, the type registry and the type name disambiguator, which is a
54 * superset of the plain mutex semantics.
55 *
56 * Outside of a ParallelSection this degrades to a load of a global plus a well-predicted branch, which keeps the
57 * single-threaded stages at their current speed.
58 */
59 public type ConditionalLock struct {
60 7769748 RecursiveMutex* mutex = nil<RecursiveMutex*>
61 }
62
63 7769748 public p ConditionalLock.ctor(RecursiveMutex* mutex) {
64
2/2
✓ Branch 0 taken 7725243 times.
✓ Branch 1 taken 44505 times.
7769748 if areConcurrentPassesRunning() {
65 44505 this.mutex = mutex;
66 44505 this.mutex.acquire();
67 44505 }
68 7769748 }
69
70 7769748 public p ConditionalLock.dtor() {
71
2/2
✓ Branch 0 taken 7725243 times.
✓ Branch 1 taken 44505 times.
7769748 if this.mutex != nil<RecursiveMutex*> {
72 44505 this.mutex.release();
73 44505 this.mutex = nil<RecursiveMutex*>;
74 44505 }
75 7769748 }
76
77 // Active symbol registry mutex. It guards the process-wide symbol lookup machinery: the lookup caches of the
78 // FunctionManager, StructManager, InterfaceManager and UnionManager, plus the manifestation lists and symbol table entries
79 // those matchers touch. It is owned by the GlobalResourceManager, which registers it here.
80 RecursiveMutex* ACTIVE_SYMBOL_REGISTRY_MUTEX;
81
82 /**
83 * Make the given mutex the active symbol registry mutex
84 *
85 * @param mutex Symbol registry mutex
86 */
87 1055 public p setActiveSymbolRegistryMutex(RecursiveMutex* mutex) {
88 1055 ACTIVE_SYMBOL_REGISTRY_MUTEX = mutex;
89 1055 }
90
91 /**
92 * Get the active symbol registry mutex
93 *
94 * The IR generator lowers struct and interface types through QualType.getStruct/getInterface and looks up implicit
95 * functions (e.g. copy ctors), so all matchers are reachable from the back-end worker threads. By the time the back end
96 * runs, the type checker has already created every manifestation, so these calls degenerate to cache hits and the lock is
97 * barely contended. It is recursive, because struct matching recurses into struct and interface matching for field and
98 * interface types.
99 *
100 * @return Active symbol registry mutex
101 */
102 406668 public f<RecursiveMutex*> getSymbolRegistryMutex() {
103 406668 assert ACTIVE_SYMBOL_REGISTRY_MUTEX != nil<RecursiveMutex*>;
104 406668 return ACTIVE_SYMBOL_REGISTRY_MUTEX;
105 }
106
107 // ─── tests ────────────────────────────────────────────────────────────────────
108
109 1 #[test]
110 f<bool> testConditionalLockOnlyLocksInParallelSection() {
111 1 RecursiveMutex mutex;
112 1 assert !areConcurrentPassesRunning();
113
114 { // Outside of a parallel section the lock is a no-op
115 1 ConditionalLock lock = ConditionalLock(&mutex);
116 1 assert lock.mutex == nil<RecursiveMutex*>;
117 1 }
118
119 { // Inside a parallel section the mutex is held for the lifetime of the lock
120 1 ParallelSection _;
121 1 assert areConcurrentPassesRunning();
122 1 ConditionalLock lock = ConditionalLock(&mutex);
123 1 assert lock.mutex == &mutex;
124 1 }
125
126 1 assert !areConcurrentPassesRunning();
127 1 return true;
128 1 }
129
130 // Shared map and the mutex that guards it. Bundled, so that the async lambdas below only have to capture a single pointer
131 type GuardedSharedMap struct {
132 UnorderedMap<unsigned long, unsigned long>* sharedMap
133 RecursiveMutex* mutex
134 }
135
136 // Increment the counter of the given key, guarded by a ConditionalLock
137 40000 p incrementGuarded(GuardedSharedMap* guardedMap, unsigned long key) {
138 40000 ConditionalLock _ = ConditionalLock(guardedMap.mutex);
139 40000 UnorderedMap<unsigned long, unsigned long>* sharedMap = guardedMap.sharedMap;
140
2/2
✓ Branch 0 taken 39500 times.
✓ Branch 1 taken 500 times.
40000 if sharedMap.contains(key) {
141 39500 sharedMap.upsert(key, sharedMap.get(key) + 1l);
142 39500 } else {
143 500 sharedMap.upsert(key, 1l);
144 500 }
145 40000 }
146
147 // Increment the counters of the given map from multiple threads. The lock is taken in a separate function, because a ctor call
148 // within a lambda body captures the struct symbol, which crashes the IR generator
149 1 p incrementConcurrently(GuardedSharedMap* guardedMap) {
150 1 ThreadPool pool = ThreadPool(4s);
151
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 time.
5 for unsigned int i = 0; i < 4; i++ {
152 8 pool.enqueue(p() [[async]] {
153
2/2
✓ Branch 0 taken 40000 times.
✓ Branch 1 taken 4 times.
40004 for unsigned long key = 0l; key < 10000l; key++ {
154 40000 incrementGuarded(guardedMap, key % 500l);
155 40000 }
156 4 });
157 4 }
158 1 pool.start();
159 1 pool.join();
160 1 }
161
162 1 #[test]
163 f<bool> testConditionalLockGuardsSharedMapUnderContention() {
164 1 UnorderedMap<unsigned long, unsigned long> sharedMap; // Stands in for the type registry / lookup caches
165 1 RecursiveMutex mutex;
166 {
167 1 ParallelSection _;
168 1 GuardedSharedMap guardedMap = GuardedSharedMap{&sharedMap, &mutex};
169 1 incrementConcurrently(&guardedMap);
170 1 }
171 1 assert !areConcurrentPassesRunning();
172
173 1 assert sharedMap.getSize() == 500l;
174
2/2
✓ Branch 0 taken 500 times.
✓ Branch 1 taken 1 time.
501 for unsigned long key = 0l; key < 500l; key++ {
175 500 assert sharedMap.get(key) == 4l * 20l;
176 500 }
177 1 return true;
178 1 }
179