
Приветствую!
Код тут
В моей библиотеке ядра уже есть минимальные, но работающие аллокатор, семейство vsnprintf, ctype, string.h, strings.h. Остались только атомики и спинлок, и тогда можно будет доделать семейство malloc в блокировками как того требует стандарт. Так уж вышло что в osdev-libstdc есть и библиотечные вещи для чистого С++ с шаблонами. Это разумный минимум для реализации последующих вещей необходимых хотя бы для того, что бы комфортно напечатать "Hello world" не говоря уже о том, что бы сделать минимально разумные контейнеры.
Реализация базового шаблона std::atomic со всеми перегрузками минимального набора функций из cppreference выполнена при помощи gcc atomic builtins. По сути сам шаблон является оберткой над ними. Это должно успешно работать на x86, x86_64, arm, arm64 и других поддерживаемых gcc платформх благодаря усилиям компилятора. На момент написания статьи код не скомпилируется ни чем кроме GCC. Поддержка clang планируется в будущем, но когда именно пока неизвестно :-)
Раз уж я обозреваю процесс разработки своей ОС, а не пишу учебник, в этой статье не будет подробно рассматриваться устройство кеша процессора или протокол MESI, я расскажу лишь о вещах необходимых для понимания что означают те или иные константы и зачем они нужны. Так же я расскажу почему они добавлены именно в atomic.h.
Все по порядку.
Что такое GCC Atomic Builtins?
Это набор встроенных в компилятор функций реализующих атомарный доступ к памяти примерно соответствующий модели памяти С++ 11. Тем кто смотрел в код std::atomic в libstdc++ gcc уже известно, что эти функции используются в реализации std::atomic. По сути моя реализация делает тоже самое, только более прямолинейно.
В реализации я использую следующие встроенные константы и функции GCC:
__ATOMIC_RELAXED
Операция атомарна, но не накладывает дополнительных ограничений на порядок других операций памяти.
__ATOMIC_ACQUIRE
Acquire-операция. Если она читает значение, записанное release-операцией, то образуется synchronizes-with / inter-thread happens-before связь.
Не позволяет последующим операциям чтения и записи быть переупорядоченными перед acquire-операцией.
__ATOMIC_RELEASE
Release-операция. Публикует все операции чтения и записи, выполненные до неё,
для потока, который потом выполнит соответствующий acquire.
Не позволяет предыдущим операциям чтения и записи быть переупорядоченными
после release-операции.
__ATOMIC_ACQ_REL
Комбинация acquire и release. Обычно используется для read-modify-write операций:
exchange, compare_exchange, fetch_add и т.д. Эти операции в базовом шаблоне реализованы не в полном объеме (пока что).
__ATOMIC_SEQ_CST Самая строгая последовательно согласованная модель памяти. Она используется по умолчанию.
В моем коде std::memory_order реализован именно через эти константы:
enum memory_order { memory_order_relaxed = __ATOMIC_RELAXED, memory_order_acquire = __ATOMIC_ACQUIRE, memory_order_release = __ATOMIC_RELEASE, memory_order_acq_rel = __ATOMIC_ACQ_REL, memory_order_seq_cst = __ATOMIC_SEQ_CST };
memory_order_consume игнорируется умышленно так как практически не используется (если верить книге Райнера Гримма) и будет удален в С++ 26.
Builtin функции следующие:
// Атомарно сохраняет значение val в *ptr следуя модели памяти mem_order void __atomic_store (type *ptr, type *val, int memorder) // Атомарно загружает значение ptr в ret ледуя модели памяти mem_order void __atomic_load (type *ptr, type *ret, int memorder) // Атомарно заменяет значение ptr на val возвращая старое значение в ret void __atomic_exchange (type *ptr, type *val, type *ret, int memorder) // Что называется CAS. Атомарно сравнивает значение ptr с exptected и если они равны // то атомарно присваивает ptr значение desired используя success_memorder в случае // совпадения значений exptected и ptr, и failure_memorder для загрузки если CAS // не прошел. bool __atomic_compare_exchange (type *ptr, type *expected, type *desired, bool weak, int success_memorder, int failure_memorder) // Проверяет можно ли реализовать вышеуказанные операции без блокировок, возвращает true // если да bool __atomic_is_lock_free (size_t size, void *ptr)
В документации написано:
The ‘__atomic’ builtins can be used with any integral scalar or pointer type that is 1, 2, 4, or 8 bytes in length. 16-byte integral types are also allowed if ‘__int128’ (see 128-bit Integers) is supported by the architecture.
Что означает что эти функции могут быть использованы для любых литералов или целых типов размером 1,2,4 или 8 байт включая указатели, что очень хорошо подходит для реализации общего шаблона std::atomic. Для таких типов компилятор обычно генерирует встроенные атомарные инструкции. Что означает что функция
bool __atomic_is_lock_free (size_t size, void *ptr)
должна возвращать true для них.
Я умышленно не использовал аналоги с постфиксом _n такие как например:
void __atomic_store_n (type *ptr, type val, int memorder)
которые принимают type по значению потому что пишу общий шаблон не только для скаляров. Так же в документации написано:
The four non-arithmetic functions (load, store, exchange, and compare_exchange) all have a generic version as well. This generic version works on any data type. It uses the lock-free built-in function if the specific data type size makes that possible; otherwise, an external call is left to be resolved at run time. This external call is the same format with the addition of a ‘size_t’ parameter inserted as the first parameter indicating the size of the object being pointed to. All objects must be the same size.
Что означает, что для типов перечисленных выше компилятор сгенерирует lock-free вызовы, иначе может быть вызвана внешняя функция с параметром size_t. В любом случае общий шаблон должен как то разрешать работу с другими тривиально копируемыми типами хотя бы в теории.
Вот и код:
__BEGIN_STD_NAMESPACE // Константа рекомендуемая для выравнивания типов если мы хотим что бы они // 100% оказались в разных линиях кеша inline constexpr size_t hardware_destructive_interference_size = __GCC_DESTRUCTIVE_SIZE; // Константа напротив рекомендует хранить тип выровненный по границе значения этой // константы в одной линейке кеша inline constexpr size_t hardware_constructive_interference_size = __GCC_CONSTRUCTIVE_SIZE; // Само собой берем выше перечисленные константы из компилятора, libstdc++ gcc делает // тоже самое, компилятор собранный для конкретной платформы знает эти значения // давайте просто поверим ему :-) // Все это определено тут что бы скрыть компилятрно специфичные вещи в одном // заголовочном файле. enum memory_order { // как описано выше memory_order_relaxed = __ATOMIC_RELAXED, memory_order_acquire = __ATOMIC_ACQUIRE, memory_order_release = __ATOMIC_RELEASE, memory_order_acq_rel = __ATOMIC_ACQ_REL, memory_order_seq_cst = __ATOMIC_SEQ_CST }; namespace detail { static memory_order __cas_failure_order(memory_order order) noexcept { switch (order) { case memory_order_relaxed: return memory_order_relaxed; case memory_order_acquire: return memory_order_acquire; case memory_order_release: return memory_order_relaxed; case memory_order_acq_rel: return memory_order_acquire; case memory_order_seq_cst: return memory_order_seq_cst; } __STD_NAMESPACE::abort(); } } // namespace detail template<typename _Type> class atomic { public: // атомики не копируются atomic(const atomic &) = delete; atomic &operator=(const atomic &) = delete; atomic &operator=(const atomic &) volatile = delete; atomic() = default; atomic(_Type __v) : _M_value(__v) {} _Type operator=(_Type __desired) noexcept { __atomic_store(&_M_value, &__desired, memory_order::memory_order_seq_cst); return __desired; } _Type operator=(_Type __desired) volatile noexcept { __atomic_store(&_M_value, &__desired, memory_order::memory_order_seq_cst); return __desired; } bool is_lock_free() const noexcept { return __atomic_is_lock_free(sizeof(_Type), nullptr); } bool is_lock_free() const volatile noexcept { return __atomic_is_lock_free(sizeof(_Type), nullptr); } void store(_Type __desired, __STD_NAMESPACE::memory_order __order = __STD_NAMESPACE::memory_order_seq_cst) noexcept { __atomic_store(&_M_value, &__desired, __order); } void store(_Type __desired, __STD_NAMESPACE::memory_order __order = __STD_NAMESPACE::memory_order_seq_cst) volatile noexcept { __atomic_store(&_M_value, &__desired, __order); } _Type load(__STD_NAMESPACE::memory_order __order = __STD_NAMESPACE::memory_order_seq_cst) const noexcept { _Type ret; __atomic_load(&_M_value, &ret, __order); return ret; } _Type load(__STD_NAMESPACE::memory_order __order = __STD_NAMESPACE::memory_order_seq_cst) const volatile noexcept { _Type ret; __atomic_load(&_M_value, &ret, __order); return ret; } operator _Type() const noexcept { _Type ret; __atomic_load(&_M_value, &ret, memory_order::memory_order_seq_cst); return ret; } operator _Type() const volatile noexcept { _Type ret; __atomic_load(&_M_value, &ret, memory_order::memory_order_seq_cst); return ret; } _Type exchange(_Type __desired, __STD_NAMESPACE::memory_order __order = __STD_NAMESPACE::memory_order_seq_cst) noexcept { _Type ret; __atomic_exchange(&_M_value, &__desired, &ret, __order); return ret; } _Type exchange(_Type __desired, __STD_NAMESPACE::memory_order __order = __STD_NAMESPACE::memory_order_seq_cst) volatile noexcept { _Type ret; __atomic_exchange(&_M_value, &__desired, &ret, __order); return ret; } bool compare_exchange_weak(_Type &expected, _Type __desired, __STD_NAMESPACE::memory_order __success, __STD_NAMESPACE::memory_order __failure) noexcept { return __atomic_compare_exchange(&_M_value, &expected, &__desired, true, __success, __failure); } bool compare_exchange_weak(_Type &expected, _Type __desired, __STD_NAMESPACE::memory_order __success, __STD_NAMESPACE::memory_order __failure) volatile noexcept { return __atomic_compare_exchange(&_M_value, &expected, &__desired, true, __success, __failure); } bool compare_exchange_weak(_Type &expected, _Type __desired, __STD_NAMESPACE::memory_order __order = __STD_NAMESPACE::memory_order_seq_cst) noexcept { return __atomic_compare_exchange(&_M_value, &expected, &__desired, true, __order, detail::__cas_failure_order(__order)); } bool compare_exchange_weak(_Type &expected, _Type __desired, __STD_NAMESPACE::memory_order __order = __STD_NAMESPACE::memory_order_seq_cst) volatile noexcept { return __atomic_compare_exchange(&_M_value, &expected, &__desired, true, __order, detail::__cas_failure_order(__order)); } bool compare_exchange_strong(_Type &expected, _Type __desired, __STD_NAMESPACE::memory_order __success, __STD_NAMESPACE::memory_order __failure) noexcept { return __atomic_compare_exchange(&_M_value, &expected, &__desired, false, __success, __failure); } bool compare_exchange_strong(_Type &expected, _Type __desired, __STD_NAMESPACE::memory_order __success, __STD_NAMESPACE::memory_order __failure) volatile noexcept { return __atomic_compare_exchange(&_M_value, &expected, &__desired, false, __success, __failure); } bool compare_exchange_strong(_Type &expected, _Type __desired, __STD_NAMESPACE::memory_order __order = __STD_NAMESPACE::memory_order_seq_cst) noexcept { return __atomic_compare_exchange(&_M_value, &expected, &__desired, false, __order, detail::__cas_failure_order(__order)); } bool compare_exchange_strong (_Type &expected, _Type __desired, __STD_NAMESPACE::memory_order __order = __STD_NAMESPACE::memory_order_seq_cst) volatile noexcept { return __atomic_compare_exchange(&_M_value, &expected, &__desired, false, __order, detail::__cas_failure_order(__order)); } private: _Type _M_value{}; }; __END_STD_NAMESPACE
Зачем CAS две модели памяти?
CAS состоит из двух операций: чтение и запись. Вот тут как раз и может быть использована модель памяти memory_order_acq_rel. Что означает memory_order_acquire для чтения и если все успешно memory_order_release для записи, но если сравнение вернуло false, то никакой записи не происходит.
Вот тут мы приходим к функции
static memory_order __cas_failure_order(memory_order order) noexcept { switch (order) { case memory_order_release: return memory_order_relaxed; case memory_order_acq_rel: return memory_order_acquire; default: return order; } }
Вот цитата из документации к функции __atomic_compare_exchange_n (функция без постфикса_n ссылается на нее):
If desired is written into *ptr then true is returned and memory is affected according to the memory order specified by success_memorder. There are no restrictions on what memory order can be used here.
Otherwise, false is returned and memory is affected according to failure_memorder. This memory order cannot be __ATOMIC_RELEASE nor __ATOMIC_ACQ_REL. It also cannot be a stronger order than that specified by success_memorder.
Что означает буквально следующее:
если desired записан в *ptr тогда функция возвращает true и операции с памятью выполняются в соответствии с моделью памяти success_memorder. Иначе возвращается false и операции с памятью выполняются в соответствии с моделью памяти failure_memorder при чем эта модель не может быть сильнее чем success_memorder и не может быть memorder_release или memorder_acq_rel от сюда и функция.
Ну и зачем же мне такой атомик? Для реализации спинлока.
__BEGIN_STD_NAMESPACE template<typename _Lock> class lock_guard final { public: lock_guard(const lock_guard &) = delete; lock_guard &operator= (const lock_guard &) = delete; lock_guard(_Lock &__lock) : _M_lock(__lock) { _M_lock.lock(); } ~lock_guard() { _M_lock.unlock(); } private: _Lock &_M_lock; }; __END_STD_NAMESPACE namespace dux { static constexpr int kFree = 0; static constexpr int kLocked = 1; class spin_lock final { public: spin_lock() = default; spin_lock(const spin_lock &) = delete; spin_lock &operator=(const spin_lock &) = delete; bool try_lock() { int expected = kFree; return _M_value.compare_exchange_strong(expected, kLocked, __STD_NAMESPACE::memory_order_acquire); } void lock() { for (;;) { while (_M_value.load(__STD_NAMESPACE::memory_order_relaxed) == kLocked) { cpu_relax(); } int expected = kFree; if (_M_value.compare_exchange_strong(expected, kLocked, __STD_NAMESPACE::memory_order_acquire)) { return; } cpu_relax(); } } void unlock() { _M_value.store(kFree, __STD_NAMESPACE::memory_order_release); } private: #ifndef OSDEV_DISABLE_SPINLOCK_ALIGNMENT alignas(__STD_NAMESPACE::hardware_destructive_interference_size) #endif __STD_NAMESPACE::atomic<int> _M_value = kFree; }; } // namespace dux
Вот теперь обсудим код. Я расскажу что и зачем, возможно кто то в комментариях меня поправит :-) Эффективно использовать примитивы синхронизации != уметь хорошо их писать. Большинство программистов используют теоретические знания из книг не вникая что под капотом. Начнем с выравнивания. Оно полезно когда разные потоки работают с разными спинлоками. Нужно убедиться что там нет false sharing. Вот бенчмарк:
#include <benchmark/benchmark.h> #include <cstddef> #include <cstdint> #include <thread> #include "spin_lock.h" namespace { static int max_threads() { const auto n = std::thread::hardware_concurrency(); return n == 0 ? 8 : static_cast<int>(n); } struct padded_counter { #ifndef DISABLE_SPINLOCK_ALIGNMENT alignas(std::hardware_destructive_interference_size) #endif std::uint64_t value = 0; }; dux::spin_lock g_lock; std::uint64_t g_shared_val = 0; dux::spin_lock g_locks[64]; padded_counter g_counters[64]; static void BM_spin_lock_lock_unlock(benchmark::State &state) { for (auto _ : state) { g_lock.lock(); benchmark::ClobberMemory(); g_lock.unlock(); } } BENCHMARK(BM_spin_lock_lock_unlock) ->Threads(1) ->Threads(2) ->Threads(4) ->Threads(8) ->Threads(16) ->UseRealTime(); static void BM_spin_lock_increment(benchmark::State &state) { for (auto _ : state) { g_lock.lock(); benchmark::DoNotOptimize(g_shared_val); ++g_shared_val; benchmark::DoNotOptimize(g_shared_val); g_lock.unlock(); } } BENCHMARK(BM_spin_lock_increment) ->Threads(1) ->Threads(2) ->Threads(4) ->Threads(8) ->Threads(16) ->UseRealTime(); static void BM_spin_lock_try_lock(benchmark::State &state) { std::uint64_t acquired = 0; std::uint64_t failed = 0; for (auto _ : state) { if (g_lock.try_lock()) { ++acquired; benchmark::DoNotOptimize(acquired); g_lock.unlock(); } else { ++failed; benchmark::DoNotOptimize(failed); } } state.counters["acquired"] = static_cast<double>(acquired); state.counters["failed"] = static_cast<double>(failed); } BENCHMARK(BM_spin_lock_try_lock) ->Threads(1) ->Threads(2) ->Threads(4) ->Threads(8) ->Threads(16) ->UseRealTime(); static void BM_spin_lock_independent_lock_unlock(benchmark::State &state) { const auto index = static_cast<std::size_t>(state.thread_index()); auto &lock = g_locks[index]; for (auto _ : state) { lock.lock(); benchmark::ClobberMemory(); lock.unlock(); } } BENCHMARK(BM_spin_lock_independent_lock_unlock) ->Threads(1) ->Threads(2) ->Threads(4) ->Threads(8) ->Threads(16) ->UseRealTime(); static void BM_spin_lock_independent_increment(benchmark::State &state) { const auto index = static_cast<std::size_t>(state.thread_index()); auto &lock = g_locks[index]; auto &counter = g_counters[index].value; for (auto _ : state) { lock.lock(); benchmark::DoNotOptimize(counter); ++counter; benchmark::DoNotOptimize(counter); lock.unlock(); } } BENCHMARK(BM_spin_lock_independent_increment) ->Threads(1) ->Threads(2) ->Threads(4) ->Threads(8) ->Threads(16) ->UseRealTime(); } // namespace
Вот вывод:
dmitry@DESKTOP-P6O5NDM:/mnt/d/osdev/osdev-libstdc/build$ benchmarks/spin_lock_unaligned_bench 2026-06-28T03:46:36+03:00 Running benchmarks/spin_lock_unaligned_bench Run on (8 X 2592 MHz CPU s) CPU Caches: L1 Data 32 KiB (x4) L1 Instruction 32 KiB (x4) L2 Unified 256 KiB (x4) L3 Unified 6144 KiB (x1) Load Average: 0.34, 0.33, 0.37 ---------------------------------------------------------------------------------------------------- Benchmark Time CPU Iterations ---------------------------------------------------------------------------------------------------- BM_spin_lock_lock_unlock/real_time/threads:1 10.3 ns 10.3 ns 64582112 BM_spin_lock_lock_unlock/real_time/threads:2 60.3 ns 60.3 ns 17700950 BM_spin_lock_lock_unlock/real_time/threads:4 207 ns 207 ns 3742600 BM_spin_lock_lock_unlock/real_time/threads:8 481 ns 480 ns 1585560 BM_spin_lock_lock_unlock/real_time/threads:16 1401 ns 838 ns 2109376 BM_spin_lock_increment/real_time/threads:1 10.9 ns 10.8 ns 63073209 BM_spin_lock_increment/real_time/threads:2 64.0 ns 64.0 ns 18999582 BM_spin_lock_increment/real_time/threads:4 338 ns 338 ns 2112388 BM_spin_lock_increment/real_time/threads:8 976 ns 976 ns 803088 BM_spin_lock_increment/real_time/threads:16 1716 ns 1058 ns 399536 BM_spin_lock_try_lock/real_time/threads:1 10.6 ns 10.6 ns 63413468 acquired=63.4135M failed=0 BM_spin_lock_try_lock/real_time/threads:2 38.4 ns 38.4 ns 13378522 acquired=7.33797M failed=6.04055M BM_spin_lock_try_lock/real_time/threads:4 86.6 ns 86.6 ns 7722064 acquired=2.38821M failed=5.33386M BM_spin_lock_try_lock/real_time/threads:8 174 ns 174 ns 3800696 acquired=759.232k failed=3.04146M BM_spin_lock_try_lock/real_time/threads:16 271 ns 160 ns 2954560 acquired=355.828k failed=2.59873M BM_spin_lock_independent_lock_unlock/real_time/threads:1 11.1 ns 11.1 ns 62118911 BM_spin_lock_independent_lock_unlock/real_time/threads:2 12.0 ns 12.0 ns 54042118 BM_spin_lock_independent_lock_unlock/real_time/threads:4 13.0 ns 13.0 ns 40000000 BM_spin_lock_independent_lock_unlock/real_time/threads:8 15.3 ns 15.3 ns 46448664 BM_spin_lock_independent_lock_unlock/real_time/threads:16 22.9 ns 14.1 ns 31888608 BM_spin_lock_independent_increment/real_time/threads:1 7.55 ns 7.55 ns 95136873 BM_spin_lock_independent_increment/real_time/threads:2 37.9 ns 37.9 ns 19401330 BM_spin_lock_independent_increment/real_time/threads:4 111 ns 111 ns 7266824 BM_spin_lock_independent_increment/real_time/threads:8 156 ns 156 ns 4608392 BM_spin_lock_independent_increment/real_time/threads:16 202 ns 121 ns 3778864 dmitry@DESKTOP-P6O5NDM:/mnt/d/osdev/osdev-libstdc/build$ benchmarks/spin_lock_aligned_bench 2026-06-28T03:46:51+03:00 Running benchmarks/spin_lock_aligned_bench Run on (8 X 2592 MHz CPU s) CPU Caches: L1 Data 32 KiB (x4) L1 Instruction 32 KiB (x4) L2 Unified 256 KiB (x4) L3 Unified 6144 KiB (x1) Load Average: 0.48, 0.36, 0.38 ---------------------------------------------------------------------------------------------------- Benchmark Time CPU Iterations ---------------------------------------------------------------------------------------------------- BM_spin_lock_lock_unlock/real_time/threads:1 10.6 ns 10.6 ns 66394512 BM_spin_lock_lock_unlock/real_time/threads:2 70.2 ns 70.2 ns 18677182 BM_spin_lock_lock_unlock/real_time/threads:4 204 ns 204 ns 4770612 BM_spin_lock_lock_unlock/real_time/threads:8 376 ns 376 ns 1793640 BM_spin_lock_lock_unlock/real_time/threads:16 1105 ns 641 ns 1086608 BM_spin_lock_increment/real_time/threads:1 11.3 ns 11.3 ns 62477240 BM_spin_lock_increment/real_time/threads:2 88.7 ns 88.7 ns 8700034 BM_spin_lock_increment/real_time/threads:4 261 ns 261 ns 2785552 BM_spin_lock_increment/real_time/threads:8 939 ns 938 ns 808024 BM_spin_lock_increment/real_time/threads:16 2206 ns 1242 ns 339232 BM_spin_lock_try_lock/real_time/threads:1 10.6 ns 10.6 ns 65253768 acquired=65.2538M failed=0 BM_spin_lock_try_lock/real_time/threads:2 37.1 ns 37.1 ns 24611966 acquired=12.8617M failed=11.7503M BM_spin_lock_try_lock/real_time/threads:4 77.7 ns 77.7 ns 7477224 acquired=2.39673M failed=5.0805M BM_spin_lock_try_lock/real_time/threads:8 165 ns 165 ns 4384976 acquired=754.444k failed=3.63053M BM_spin_lock_try_lock/real_time/threads:16 254 ns 148 ns 3188912 acquired=402.998k failed=2.78591M BM_spin_lock_independent_lock_unlock/real_time/threads:1 10.6 ns 10.6 ns 71575886 BM_spin_lock_independent_lock_unlock/real_time/threads:2 12.1 ns 12.1 ns 60238786 BM_spin_lock_independent_lock_unlock/real_time/threads:4 13.1 ns 13.1 ns 40000000 BM_spin_lock_independent_lock_unlock/real_time/threads:8 14.9 ns 14.9 ns 46986640 BM_spin_lock_independent_lock_unlock/real_time/threads:16 22.4 ns 14.1 ns 30550288 BM_spin_lock_independent_increment/real_time/threads:1 12.2 ns 12.2 ns 57258680 BM_spin_lock_independent_increment/real_time/threads:2 13.0 ns 13.0 ns 53044384 BM_spin_lock_independent_increment/real_time/threads:4 13.7 ns 13.7 ns 40000000 BM_spin_lock_independent_increment/real_time/threads:8 14.8 ns 14.8 ns 49200832 BM_spin_lock_independent_increment/real_time/threads:16 22.2 ns 13.9 ns 32020000
Мы видим что как я и сказал выше секция BM_spin_lock_independent работает быстрее с вырваниванием. Конкретно в osdev-libstdc можно использовать макрос OSDEV_DISABLE_SPINLOCK_ALIGNMENT что бы его отключить. Идем дальше.
Логика захвата TTAS (test-test-and-set), а не TAS (test-and-set) как в классическом спинлоке реализуемом через std::atomic_flag. Буквально это означает что при попытке захватить лок поток сначала проверяет свободен ли он выполняя операцию load, что не требует переключения кеш линии в состояние modified. Каждый раз в цикле ожидания.
Exchange осуществляется только если лок свободен что снижает нагрузку на кеш. exchage это одна атомарная операция поэтому поток всегда пытается получить право на запись кеш линии не зависимо от того совпадает текущее значение с желаемым или нет. Если бы при выполнении exhange сначала выполнялось чтение и принималось бы решение нужно право на запись или нет, атомарность операции была бы нарушена. Т.е. архитектурно это операция единая атомарная read-modify-write когда ЦПУ не может сначала выполнить чтение и потом решать нужна ли запись. Это архитектурная необходимость. Что означает что другой ЦПУ мог бы изменить значение между чтением и принятием решения нужно ли право на запись.
Вот TTAS графически:

Вот бенчмарк
#include <benchmark/benchmark.h> #include "../include/atomic.h" #include <asm/cpu.h> #include <cstdint> #include <thread> namespace { class tas_spin_lock { public: static constexpr int kFree = 0; static constexpr int kLocked = 1; bool try_lock() { return _M_value.exchange(kLocked, __STD_NAMESPACE::memory_order_acquire) == kFree; } void lock() { while (_M_value.exchange(kLocked, __STD_NAMESPACE::memory_order_acquire) != kFree) { cpu_relax(); } } void unlock() { _M_value.store(kFree, __STD_NAMESPACE::memory_order_release); } private: alignas(__STD_NAMESPACE::hardware_destructive_interference_size) __STD_NAMESPACE::atomic<int> _M_value = kFree; }; class ttas_spin_lock { public: static constexpr int kFree = 0; static constexpr int kLocked = 1; bool try_lock() { return _M_value.exchange(kLocked, __STD_NAMESPACE::memory_order_acquire) == kFree; } void lock() { for (;;) { while (_M_value.load(__STD_NAMESPACE::memory_order_relaxed) == kLocked) { cpu_relax(); } if (_M_value.exchange(kLocked, __STD_NAMESPACE::memory_order_acquire) == kFree) { return; } } } void unlock() { _M_value.store(kFree, __STD_NAMESPACE::memory_order_release); } private: alignas(__STD_NAMESPACE::hardware_destructive_interference_size) __STD_NAMESPACE::atomic<int> _M_value = kFree; }; template <typename Lock> struct bench_data { Lock lock; std::uint64_t shared_val = 0; }; bench_data<tas_spin_lock> g_tas_data; bench_data<ttas_spin_lock> g_ttas_data; template <typename Lock> static void BM_lock_unlock(benchmark::State &state, bench_data<Lock> &data) { for (auto _ : state) { data.lock.lock(); benchmark::ClobberMemory(); data.lock.unlock(); } } template <typename Lock> static void BM_increment(benchmark::State &state, bench_data<Lock> &data) { for (auto _ : state) { data.lock.lock(); benchmark::DoNotOptimize(data.shared_val); ++data.shared_val; benchmark::DoNotOptimize(data.shared_val); data.lock.unlock(); } } static void BM_TAS_lock_unlock(benchmark::State &state) { BM_lock_unlock(state, g_tas_data); } BENCHMARK(BM_TAS_lock_unlock) ->Threads(1) ->Threads(2) ->Threads(4) ->Threads(8) ->Threads(16) ->UseRealTime(); static void BM_TTAS_lock_unlock(benchmark::State &state) { BM_lock_unlock(state, g_ttas_data); } BENCHMARK(BM_TTAS_lock_unlock) ->Threads(1) ->Threads(2) ->Threads(4) ->Threads(8) ->Threads(16) ->UseRealTime(); static void BM_TAS_increment(benchmark::State &state) { BM_increment(state, g_tas_data); } BENCHMARK(BM_TAS_increment) ->Threads(1) ->Threads(2) ->Threads(4) ->Threads(8) ->Threads(16) ->UseRealTime(); static void BM_TTAS_increment(benchmark::State &state) { BM_increment(state, g_ttas_data); } BENCHMARK(BM_TTAS_increment) ->Threads(1) ->Threads(2) ->Threads(4) ->Threads(8) ->Threads(16) ->UseRealTime(); } // namespace
Вот вывод:
dmitry@DESKTOP-P6O5NDM:/mnt/d/osdev/osdev-libstdc/build$ benchmarks/spin_ttas_vs_tas_bench 2026-06-28T05:00:27+03:00 Running benchmarks/spin_ttas_vs_tas_bench Run on (8 X 2592 MHz CPU s) CPU Caches: L1 Data 32 KiB (x4) L1 Instruction 32 KiB (x4) L2 Unified 256 KiB (x4) L3 Unified 6144 KiB (x1) Load Average: 0.05, 0.01, 0.00 ----------------------------------------------------------------------------------- Benchmark Time CPU Iterations ----------------------------------------------------------------------------------- BM_TAS_lock_unlock/real_time/threads:1 10.8 ns 10.8 ns 63718850 BM_TAS_lock_unlock/real_time/threads:2 62.1 ns 62.1 ns 12398532 BM_TAS_lock_unlock/real_time/threads:4 317 ns 317 ns 3311780 BM_TAS_lock_unlock/real_time/threads:8 809 ns 809 ns 1074088 BM_TAS_lock_unlock/real_time/threads:16 2821 ns 1567 ns 274768 BM_TTAS_lock_unlock/real_time/threads:1 10.7 ns 10.7 ns 68831786 BM_TTAS_lock_unlock/real_time/threads:2 90.9 ns 90.9 ns 9534798 BM_TTAS_lock_unlock/real_time/threads:4 220 ns 220 ns 5248624 BM_TTAS_lock_unlock/real_time/threads:8 421 ns 389 ns 1257544 BM_TTAS_lock_unlock/real_time/threads:16 1036 ns 617 ns 2138256 BM_TAS_increment/real_time/threads:1 7.22 ns 7.22 ns 97365700 BM_TAS_increment/real_time/threads:2 79.5 ns 79.5 ns 9393780 BM_TAS_increment/real_time/threads:4 499 ns 499 ns 1944148 BM_TAS_increment/real_time/threads:8 1315 ns 1299 ns 1019816 BM_TAS_increment/real_time/threads:16 4356 ns 2699 ns 238320 BM_TTAS_increment/real_time/threads:1 9.30 ns 9.30 ns 97584095 BM_TTAS_increment/real_time/threads:2 44.4 ns 44.4 ns 16578246 BM_TTAS_increment/real_time/threads:4 320 ns 320 ns 2399060 BM_TTAS_increment/real_time/threads:8 955 ns 955 ns 741168 BM_TTAS_increment/real_time/threads:16 2874 ns 1631 ns 830592
В следующих статьях будем писать malloc, realloc, calloc, alloc_aligned, free со спин локом. Возможно будет обзор возможностей osdev-libstdc в целом. Если кому интересно это дело напишите в комментариях сделаю.
Далее реализация минимального libcppabi с потоко безопасной инициализацией локальных статических объектов new/delete. Мы реализуем магию new/delete и станет понято откуда delete[] знает размер массива.
До новых встреч!
