Лучший способ атомарно побитового И байта в C/С++?

В настоящее время изучаю атомарные операции в C/C++ с использованием GCC и обнаружил, что естественно выровненные глобальные переменные в памяти имеют атомарное чтение и запись.

Тем не менее, я пытался использовать побитовую И глобальную переменную и заметил, что она сводится к последовательности чтения-изменения-записи, которая вызывает проблемы, если с этим значением байта работает несколько потоков.

После некоторых исследований я остановился на этих двух примерах:

Пример C - расширение GCC __sync_fetch_and_and

#include <stdio.h>
#include <stdint.h>

uint8_t byteC = 0xFF;

int main() {
    __sync_fetch_and_and(&byteC, 0xF0);
    printf("Value of byteC: 0x%X\n", byteC);
    return 0;
}

Пример C++ - C++11 с использованием атомарного fetch_and

#include <iostream>
#include <atomic>

std::atomic<uint8_t> byteCpp(0xFF);

int main() {
    byteCpp.fetch_and(0xF0);
    std::cout << "Value of byteCpp: 0x" << std::hex << static_cast<int>(byteCpp.load()) << std::endl;
    return 0;
}

Далее следуют другие примеры, но они кажутся менее интуитивными и более затратными в вычислительном отношении.

Использование pthread_mutex_lock

uint8_t byte = 0xFF;
pthread_mutex_t byte_mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&byte_mutex);
byte &= 0xF0;
pthread_mutex_unlock(&byte_mutex);

Использование мьютекса lock_guard

#include <mutex>

uint8_t byte;
std::mutex byte_mutex;

void atomic_and() {
    std::lock_guard<std::mutex> lock(byte_mutex);
    byte &= 0xF0;
}

Использование compare_exchange_weak

std::atomic<uint8_t> byte;

void atomic_and() {
    uint8_t old_val, new_val;
    do {
        old_val = byte.load();
        new_val = old_val & 0xF0;
    } while (!byte.compare_exchange_weak(old_val, new_val));
}

Вопрос

Каков наилучший атомарный метод для последовательности чтения-изменения-записи в многопоточной программе C/C++?

«естественно выровненные глобальные переменные в памяти имеют атомарное чтение и запись» - э-э, что? Насколько я знаю, нет.

Jesper Juhl 18.08.2023 19:56

Кажется, вы задаете два разных вопроса: один для C, а другой для C++. Вероятно, в каждом случае ответ будет разным. Выберите, о каком языке вы спрашиваете, и опубликуйте отдельный вопрос для другого языка, если вам действительно нужен ответ для обоих.

François Andrieux 18.08.2023 19:57

@JesperJuhl: естественно выровненные глобальные переменные в памяти имеют атомарное чтение и запись - на ассемблере да для малой степени 2 размера на большинстве ISA, например x86 . Но это не язык ассемблера, поэтому ваше возражение справедливо. «Атомарность» в C и C++ также означает запрет оптимизатору удалять или переупорядочивать операции. Кроме того, вы определенно не получите атомарность RMW бесплатно! Атомарная чистая загрузка, а затем последующее атомарное сохранение другого значения — это совсем не атомарный RMW, поэтому в x86 есть такие инструкции, как lock and byte [mem], reg.

Peter Cordes 18.08.2023 20:00

Что такое C/C++, почему это не C++/C?

273K 18.08.2023 20:00

Это правда или нет? Intel гарантирует, что одна выровненная 32-битная запись в память будет происходить атомарно: т. е. если вы записываете в память целочисленное значение, вам не нужно беспокоиться о том, что другой поток увидит некоторые биты, но не все.

vengy 18.08.2023 20:02
__sync_fetch_and_and является встроенным компилятором, поэтому часть вопроса касается не C, а скорее gcc.
273K 18.08.2023 20:02

@JesperJuhl: Возможно, ОП имеет в виду чтение ветки комментариев к Влияет ли значение `std::memory_ordering` как на переупорядочение компилятора, так и на аппаратные инструкции для атомарных объектов? вчера, где тот факт, что доступ к простым неатомарным переменным компилируется в те же инструкции asm (например, x86 mov), что и relaxed atomics. Это не означает, что простые переменные C являются атомарными в каком-либо смысле. См. LWN: Кто боится большого плохого оптимизирующего компилятора? re: подводные камни простых переменных в ядре Linux.

Peter Cordes 18.08.2023 20:05

С11 _Atomic. 🤷

Siguza 18.08.2023 20:06

@vengy - одна запись происходит атомарно, если это происходит. Но для неатомарной переменной компилятор может сохранить значение в регистре и, возможно, записать его позже. Для операции AND вы просто потеряны, так как это и чтение, и запись. Почему бы не довериться std::atomic<uint8_t> использованию наилучшего способа выполнения операции?

BoP 18.08.2023 20:06

@BoP Спасибо. Я буду придерживаться std::atomic, так как это наименее запутанно для меня. На самом деле я думал, что запись значения в глобальную переменную, такую ​​как byte = 0, будет атомарной операцией.

vengy 18.08.2023 20:11

@vengy: «Гарантия Intel заключается в том, что одна выровненная 32-битная запись в память будет происходить атомарно»: гарантировал ли вам компилятор, что если вы присвоите значение 32-битному объекту в исходном коде C, он реализует это с одной выровненной 32-битной памятью писать?

Eric Postpischil 18.08.2023 20:13

Если вы действительно хотели использовать встроенные функции GNU C, не используйте устаревшие встроенные функции __sync. Используйте __atomic_fetch_and(&byte, 0xf0, __ATOMIC_RELAXED) или что-то еще (gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html), которые также обеспечивают атомарную чистую загрузку и атомарное чистое хранилище, вместо того, чтобы взломать *(volatile char*), чтобы принудительно загрузить или сохранить произойдет.

Peter Cordes 18.08.2023 20:13

Кроме того, x86 — не единственная архитектура, которую поддерживает C++.

Jesper Juhl 18.08.2023 20:34
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
14
85
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

[Я] обнаружил, что естественно выровненные глобальные переменные в памяти имеют атомарное чтение и запись.

Это неверно в смысле C/C++, только в смысле x86_64. Это правда, что любые выровненные загрузки и сохранения на x86_64 являются атомарными, но это неверно для абстрактной машины. Одновременная запись в неатомарный бит памяти — это всегда гонка данных, и дезинсекторы потоков могут поймать ошибку, даже если архитектура теоретически делает это безопасным.

Кроме того, лучший способ атомарного выполнения byte &= 0xf0 очень похож на C и C++:

// C++
#include <atomic>
std::atomic_uint8_t byte; // or std::atomic<std::uint8_t>
// ...
std::uint8_t old = byte.fetch_and(0xf0); /* optionally specify memory order */
// or
std::uint8_t old = std::atomic_fetch_and(&byte, 0xf0);
// C (no compiler extensions/intrinsics needed)
#include <stdatomic.h>
atomic_uint8_t byte; // or _Atomic uint8_t
// ...
uint8_t old = atomic_fetch_and(&byte, 0xf0); /* optionally atomic_fetch_and_explicit */

Другие методы (потоки POSIX, std::mutex, compare_exchange повторные циклы) почти наверняка хуже встроенного способа в виде fetch_and функций. Если в архитектуре не предусмотрена инструкция атомарной выборки-И, то следует выбрать тот способ, который лучше всего подходит. Это не то, о чем вам нужно беспокоиться.


Смотрите также

Спасибо @PeterCordes за то, что поделились этими ссылками.

Правда в сборке на большинстве ISA, не только на x86-64. Вот почему std::atomic<T>.load(relaxed)/.store(relaxed)` просто компилируется в простые инструкции загрузки и сохранения, те же самые, которые компиляторы используют для простых переменных, для T шириной регистра или меньше на большинстве ISA. Но да, совершенно ошибочное рассуждение, будучи атомарным в C или C++, также означает, что оптимизатору не нужны руки. См. LWN: Кто боится большого плохого оптимизирующего компилятора? re: подводные камни простых переменных в ядре Linux (где они используют volatile с GCC и известными типами и asm для более строгого упорядочения).

Peter Cordes 18.08.2023 20:11

Коллега спросил меня о чтении/записи глобальной переменной в C++, поэтому моей первоначальной реакцией было использование std::atomic, но другие люди сказали, что присваивание int выровненной переменной было атомарным. В любом случае, спасибо за все ответы. Мне нужно многому научиться...

vengy 18.08.2023 20:32

Другие вопросы по теме