Приветствую!

Код тут

В моей библиотеке ядра уже есть минимальные, но работающие аллокатор, семейство 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[] знает размер массива.

До новых встреч!