Как реализован atomic_flag? - PullRequest
       101

Как реализован atomic_flag?

3 голосов
/ 05 января 2020

Как это atomic_flag реализовано? Мне кажется, что на x86-64 это все равно эквивалентно atomic_bool, но это всего лишь предположение. Может ли реализация x86-64 отличаться от arm или x86?

Ответы [ 2 ]

6 голосов
/ 05 января 2020

Да, на обычных процессорах, где atomic<bool> и atomic<int> также не блокируются, это похоже на atomic<bool>, используя те же инструкции. (x86 и x86-64 имеют одинаковый набор доступных операций atomi c.)

Можно подумать, что он всегда будет использовать x86 lock bts или lock btr для установки / сброса (очистки) одного немного, но может быть более эффективно делать другие вещи (особенно для функции, которая возвращает bool вместо ветвления на нем). Объект представляет собой целый байт, поэтому вы можете просто сохранить или обменять весь байт. (И если ABI гарантирует, что значение всегда 0 или 1, вам не нужно логизировать его, прежде чем возвращать результат как bool)

G CC и clang скомпилируйте test_and_set для обмена байтами и очистите для хранилища байтов 0. Мы получаем (почти) идентичный asm для atomic_flag test_and_set как f.exchange(true);

#include <atomic>

bool TAS(std::atomic_flag &f) {
    return f.test_and_set();
}

bool TAS_bool(std::atomic<bool> &f) {
    return f.exchange(true);
}


void clear(std::atomic_flag &f) {
    //f = 0; // deleted
    f.clear();
}

void clear_relaxed(std::atomic_flag &f) {
    f.clear(std::memory_order_relaxed);
}

void bool_clear(std::atomic<bool> &f) {
    f = false; // deleted
}

На Godbolt для x86-64 с g cc и clang, а также для ARMv7 и AArch64.

## GCC9.2 -O3 for x86-64
TAS(std::atomic_flag&):
        mov     eax, 1
        xchg    al, BYTE PTR [rdi]
        ret
TAS_bool(std::atomic<bool>&):
        mov     eax, 1
        xchg    al, BYTE PTR [rdi]
        test    al, al
        setne   al                      # missed optimization, doesn't need to booleanize to 0/1
        ret
clear(std::atomic_flag&):
        mov     BYTE PTR [rdi], 0
        mfence                          # memory fence to drain store buffer before future loads
        ret
clear_relaxed(std::atomic_flag&):
        mov     BYTE PTR [rdi], 0      # x86 stores are already mo_release, no barrier
        ret
bool_clear(std::atomic<bool>&):
        mov     BYTE PTR [rdi], 0
        mfence
        ret

Обратите внимание, что xchg также является эффективным способом сделать seq_cst хранить на x86-64, обычно более эффективно, чем mov + mfence, который использует g cc. Clang использует xchg для всего этого (кроме расслабленного магазина).

Забавно, что Clang повторно логизирует 0/1 после xchg в atomic_flag.test_and_set(), но G CC вместо этого делает это после atomic<bool>. clang делает странный and al,1 в TAS_bool, который рассматривал бы значения типа 2 как ложные Это кажется совершенно бессмысленным; ABI гарантирует, что bool в памяти всегда сохраняется как 0 или 1 байт.

Для ARM у нас есть ldrexb / strexb циклов повторных попыток обмена или просто strb + dmb ish для чистого магазина. Либо AArch64 может использовать stlrb wzr, [x0] для clear или assign-false для создания хранилища с последовательным выпуском (нулевого регистра) без барьера.

0 голосов
/ 05 января 2020

На большинстве / вменяемых архитектур прерывание может произойти после или до выполнения аппаратной инструкции. Не «между» это исполнение. Таким образом, либо инструкция «происходит» (ie. С «побочными эффектами»), либо не выполняется.

Например, 16-битная архитектура, скорее всего, имеет аппаратные инструкции для работы с 16-битными переменными одной инструкцией. Таким образом, увеличение 16-битной переменной будет одной инструкцией. Сохранение значения в 16-битной переменной будет одной инструкцией. Et c. Для 16-битных переменных блокировка не нужна, поскольку приращение либо происходит, либо не происходит атомарно. На этой архитектуре невозможно наблюдать состояние «среднего исполнения» приращения 16-битной переменной. Это отдельная инструкция. Он не может быть прерван "между" никаким сигналом и прерыванием.

В 16-битной архитектуре может отсутствовать инструкция для увеличения 64-битной переменной в одной инструкции. Для выполнения операций над 64-битными переменными может потребоваться множество инструкций. Таким образом, операциям на std::atomic<uint64_t> необходимы дополнительные инструкции синхронизации, вставленные компилятором для реализации его функциональности, для реализации синхронизации с другими std::atomic переменными и т. Д. c.

Но операции с 16-битными переменными в этой архитектуре являются одиночными инструкции, компилятору не нужно ничего с ними делать, побочные эффекты всегда будут видны везде после выполнения инструкции.

Так что atomic_flag, скорее всего, просто переменная, которая имеет размер слова на конкретном процессоре. Это сделано для того, чтобы этот процессор мог работать с этой переменной с помощью одной инструкции. На практике это int, но int не обязательно соответствует размеру слова процессора, а доступ к дескрипторам int не гарантированно будет атомом c. Я считаю, что обычно atomic_flag совпадает с sig_atomic_t от posix ( posix docs ). Дополнительные atomic_flag ограничивают его операции до bool -i sh, как только: очистить, установить и уведомить.

...