Подтвердить что ты не робот

Приобретение/выпуск по сравнению с последовательным последовательным порядком памяти

Для любого 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 с точки зрения эффективности?
4b9b3361

Ответ 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, например, требуют немного больше работы в процессоре, чтобы все работало.