src/util/Concurrency.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Copyright (c) 2021-2026 ChilliBits. All rights reserved. | ||
| 2 | |||
| 3 | #pragma once | ||
| 4 | |||
| 5 | #include <atomic> | ||
| 6 | #include <mutex> | ||
| 7 | |||
| 8 | namespace spice::compiler { | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Global switch that tells the process-wide caches of the compiler (type registry, function lookup cache, ...) whether | ||
| 12 | * compiler passes are currently executed on more than one thread. | ||
| 13 | * | ||
| 14 | * Those caches sit on the hottest paths of the single-threaded front end and middle end, where taking a lock for every | ||
| 15 | * access would be pure overhead. The back end on the other hand runs one pipeline per source file in parallel (see | ||
| 16 | * SourceFile::runBackEnd), and there the caches do need protection. ParallelSection flips this switch for exactly the | ||
| 17 | * time span in which worker threads are alive. | ||
| 18 | */ | ||
| 19 | inline std::atomic concurrentPassesRunning = false; | ||
| 20 | |||
| 21 | /** | ||
| 22 | * RAII marker for a section of the compiler in which passes are executed on multiple threads. | ||
| 23 | * | ||
| 24 | * It has to be entered before the worker threads are handed any work and left only after all of them are joined again, | ||
| 25 | * so that every ConditionalLock taken on a worker thread actually locks. | ||
| 26 | */ | ||
| 27 | class ParallelSection final { | ||
| 28 | public: | ||
| 29 | // Constructors | ||
| 30 | 2 | ParallelSection() { concurrentPassesRunning.store(true, std::memory_order_relaxed); } | |
| 31 | |||
| 32 | // Prevent copy | ||
| 33 | ParallelSection(const ParallelSection &) = delete; | ||
| 34 | ParallelSection &operator=(const ParallelSection &) = delete; | ||
| 35 | |||
| 36 | // Destructor | ||
| 37 | 2 | ~ParallelSection() { concurrentPassesRunning.store(false, std::memory_order_relaxed); } | |
| 38 | }; | ||
| 39 | |||
| 40 | /** | ||
| 41 | * RAII lock that is only actually acquired while compiler passes run concurrently. | ||
| 42 | * | ||
| 43 | * Outside of a ParallelSection this degrades to a relaxed atomic load plus a well-predicted branch, which keeps the | ||
| 44 | * single-threaded stages at their current speed. | ||
| 45 | */ | ||
| 46 | template <typename MutexT> class ConditionalLock final { | ||
| 47 | public: | ||
| 48 | // Constructors | ||
| 49 |
3/4spice::compiler::ConditionalLock<std::recursive_mutex>::ConditionalLock(std::recursive_mutex&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 196202 times.
spice::compiler::ConditionalLock<std::mutex>::ConditionalLock(std::mutex&):
✓ Branch 3 → 4 taken 39673 times.
✓ Branch 3 → 5 taken 14968110 times.
|
15204123 | explicit ConditionalLock(MutexT &mutex) : mutex(concurrentPassesRunning.load(std::memory_order_relaxed) ? &mutex : nullptr) { |
| 50 |
3/4spice::compiler::ConditionalLock<std::recursive_mutex>::ConditionalLock(std::recursive_mutex&):
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 196202 times.
spice::compiler::ConditionalLock<std::mutex>::ConditionalLock(std::mutex&):
✓ Branch 6 → 7 taken 39706 times.
✓ Branch 6 → 8 taken 14968077 times.
|
15203985 | if (this->mutex != nullptr) |
| 51 | 39706 | this->mutex->lock(); | |
| 52 | 15204280 | } | |
| 53 | |||
| 54 | // Prevent copy | ||
| 55 | ConditionalLock(const ConditionalLock &) = delete; | ||
| 56 | ConditionalLock &operator=(const ConditionalLock &) = delete; | ||
| 57 | |||
| 58 | // Destructor | ||
| 59 | 15204349 | ~ConditionalLock() { | |
| 60 |
3/4spice::compiler::ConditionalLock<std::recursive_mutex>::~ConditionalLock():
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 196202 times.
spice::compiler::ConditionalLock<std::mutex>::~ConditionalLock():
✓ Branch 2 → 3 taken 40001 times.
✓ Branch 2 → 4 taken 14968146 times.
|
15204349 | if (mutex != nullptr) |
| 61 | 40001 | mutex->unlock(); | |
| 62 | 15204202 | } | |
| 63 | |||
| 64 | private: | ||
| 65 | // Members | ||
| 66 | MutexT *mutex; | ||
| 67 | }; | ||
| 68 | |||
| 69 | /** | ||
| 70 | * Guards the process-wide symbol lookup machinery: the lookup caches of FunctionManager, StructManager and | ||
| 71 | * InterfaceManager, plus the manifestation lists and symbol table entries those matchers touch. | ||
| 72 | * | ||
| 73 | * The IR generator lowers struct and interface types through QualType::getStruct/getInterface and looks up implicit | ||
| 74 | * functions (e.g. copy ctors), so all three matchers are reachable from the back-end worker threads. By the time the | ||
| 75 | * back end runs, the type checker has already created every manifestation, so these calls degenerate to cache hits and | ||
| 76 | * the lock is barely contended. | ||
| 77 | * | ||
| 78 | * It is recursive because struct matching recurses into struct and interface matching for field and interface types. | ||
| 79 | */ | ||
| 80 | inline std::recursive_mutex symbolRegistryMutex; | ||
| 81 | |||
| 82 | } // namespace spice::compiler | ||
| 83 |