Приобретение/выпуск по сравнению с последовательным последовательным порядком памяти
Для любого std::atomic<T>
, где T - примитивный тип:
Если я использую std::memory_order_acq_rel
для операций fetch_xxx
и std::memory_order_acquire
для load
и std::memory_order_release
для store
вслепую (я имею в виду, как сброс настроек памяти по умолчанию для этих функций)
- Результаты будут такими же, как если бы я использовал
std::memory_order_seq_cst
(который используется по умолчанию) для любой из объявленных операций?
- Если результаты были одинаковыми, это использование так или иначе отличается от использования
std::memory_order_seq_cst
с точки зрения эффективности?
Ответы
Ответ 1
Параметры упорядочения памяти С++ 11 для атомарных операций определяют ограничения на упорядочение. Если вы делаете хранилище с помощью std::memory_order_release
, а загрузка из другого потока считывает значение с помощью std::memory_order_acquire
то последующие операции чтения из второго потока будут видеть любые значения, сохраненные в любом месте памяти первым потоком, которые были до релиз магазина или более поздний магазин в любой из этих областей памяти.
Если и хранилище, и последующая загрузка являются std::memory_order_seq_cst
то отношения между этими двумя потоками одинаковы. Вам нужно больше потоков, чтобы увидеть разницу.
Например, переменные std::atomic<int>
x
и y
, оба изначально равны 0.
Тема 1:
x.store(1,std::memory_order_release);
Тема 2:
y.store(1,std::memory_order_release);
Тема 3:
int a=x.load(std::memory_order_acquire); // x before y
int b=y.load(std::memory_order_acquire);
Тема 4:
int c=y.load(std::memory_order_acquire); // y before x
int d=x.load(std::memory_order_acquire);
Как написано, нет никакой связи между хранилищами x
и y
, поэтому вполне возможно увидеть a==1
, b==0
в потоке 3 и c==1
и d==0
в потоке 4.
Если все упорядочения памяти изменены на std::memory_order_seq_cst
то это приводит в порядок упорядочение между хранилищами по x
и y
. Следовательно, если поток 3 видит a==1
и b==0
то это означает, что сохранение до x
должно быть до сохранения до y
, поэтому, если поток 4 видит c==1
, что означает, что сохранение до y
завершено, то Сохранение в x
также должно быть завершено, поэтому мы должны иметь d==1
.
На практике использование везде std::memory_order_seq_cst
повлечет за собой дополнительные накладные расходы либо на загрузку, либо на хранение, либо на то и другое, в зависимости от архитектуры вашего компилятора и процессора. Например, обычная техника для процессоров x86 состоит в том, чтобы использовать инструкции XCHG
вместо инструкций MOV
для хранилищ std::memory_order_seq_cst
, чтобы обеспечить необходимые гарантии упорядочения, тогда как для std::memory_order_release
достаточно простого MOV
. В системах с более расслабленной архитектурой памяти издержки могут быть больше, поскольку обычные нагрузки и хранилища имеют меньше гарантий.
Упорядочить память сложно. Я посвятил этому почти целую главу в своей книге.
Ответ 2
Порядок запоминания может быть довольно сложным, и последствия его неправильного использования часто очень тонкие.
Ключевым моментом во всем упорядочении памяти является то, что он гарантирует то, что "HAPPENED", а не то, что произойдет. Например, если вы храните что-то несколько переменных (например, x = 7; y = 11;
), тогда другой процессор может видеть y
как 11, прежде чем он увидит значение 7
в x. Используя операцию упорядочения памяти между установкой x
и установкой y
, используемый вами процессор гарантирует, что x = 7;
был записан в память до того, как он сохранит что-то в y
.
В большинстве случаев это НЕ ДЕЙСТВИТЕЛЬНО важно, чтобы упорядочить ваши записи, пока значение обновляется в конце концов. Но если мы, скажем, имеем круговой буфер с целыми числами, и мы делаем что-то вроде:
buffer[index] = 32;
index = (index + 1) % buffersize;
и какой-то другой поток использует index
, чтобы определить, что новое значение было записано, тогда нам нужно было 32
записать FIRST, а затем index
обновить ПОСЛЕ. В противном случае другой поток может получить данные old
.
То же самое касается создания семафоров, мьютексов и таких вещей - вот почему термины release и приобретать используются для типов барьеров памяти.
Теперь cst
является самым строгим правилом упорядочения - он обеспечивает, чтобы как чтение, так и запись записанных данных выходили в память, прежде чем процессор сможет продолжать делать больше операций. Это будет медленнее, чем выполнение специальных барьеров для приобретения или выпуска. Это заставляет процессор следить за тем, чтобы магазины AND были заполнены, а не просто магазины или просто загружаются.
Какая разница? Он сильно зависит от архитектуры системы. В некоторых системах кеш должен очищаться [частично], а прерывания отправляются от одного ядра к другому, чтобы сказать "Проделайте эту работу по очистке кеша до продолжения" - это может занять несколько сотен циклов. На других процессорах он лишь на небольшой процент медленнее, чем обычная запись в память. X86 неплохо справляется с этим. Некоторые типы встроенных процессоров (некоторые модели - не уверены?) ARM, например, требуют немного больше работы в процессоре, чтобы все работало.