Нет, определенно не совсем. Это, по крайней мере, барьер памяти в потоке для более сильных порядков памяти.
Для mo_relaxed
атомов, да, я думаю, что теоретически он может быть полностью оптимизирован, как если бы он не былтам в источнике. Это эквивалентно тому, что поток просто не является частью последовательности выпуска, частью которой он мог быть.
Если вы использовали результат fetch_add(0, mo_relaxed)
, то я думаю, что сворачивать их вместе и просто выполнять загрузкувместо RMW 0
может быть не совсем эквивалентным. Барьеры в этой нити, окружающие расслабленный RMW, все еще влияют на все операции, включая порядок расслабленной операции по отношению к. неатомные операции. С нагрузкой + хранилище , связанными вместе как атомарный RMW , вещи, которые заказывают хранилища, могли бы заказать атомарный RMW, если бы они не заказывали чистый груз.
Но я не думаю, чтолюбой порядок в C ++ выглядит следующим образом: mo_release
хранит порядок более ранних загрузок и сохраняет, а atomic_thread_fence(mo_release)
напоминает барьер asm StoreStore + LoadStore. ( Зависание на заборах ). Так что да, учитывая, что любой порядок, наложенный на C ++, будет также применяться к расслабленной нагрузке в равной степени к расслабленному RMW, я думаю, int tmp = shared.fetch_add(0, mo_relaxed)
можно было бы оптимизировать только под нагрузку.
( Inпрактические компиляторы вообще не оптимизируют атомики, в основном трактуя их как volatile atomic
, даже для mo_relaxed
. Почему компиляторы не объединяют избыточные записи std :: atomic? и http://wg21.link/n4455 + http://wg21.link/p0062. Слишком сложно / не существует механизма, позволяющего компиляторам знать, когда не к.)
Но да, стандарт ISO C ++ на бумаге не даетгарантировать, что другие потоки действительно могут наблюдать любое заданное промежуточное состояние.
Мысленный эксперимент: рассмотрим реализацию C ++ в одноядерной кооперативной многозадачной системе . Он реализует std::thread
, вставляя вызовы yield там, где это необходимо, чтобы избежать взаимоблокировок, но не между каждой инструкцией. Ничто в стандарте не требует выхода между num++
и num--
, чтобы позволить другим потокам наблюдать это состояние.
Правило «как будто» в основном позволяет компилятору выбирать допустимый / возможный порядок и принимать решение при компиляции. время это то, что происходит каждый раз.
На практике это может создать проблемы справедливости, если разблокировка / повторная блокировка фактически не дает другим потокам возможность взять блокировку, если --
/ ++
объединены вместев просто барьер памяти без модификации атомного объекта! Это, помимо прочего, объясняет, почему компиляторы не оптимизируют.
Любой более сильный порядок для одной или обеих операций может начинаться или быть частью последовательности выпуска, которая синхронизирует-с читателем. Читатель, который выполняет загрузку хранилища релизов / RMW Synchronizes-With этого потока, и должен видеть все предыдущие эффекты этого потока как уже произошедшие.
IDK, как читатель узнает, что он видит хранилище релизов этого потока вместо некоторого предыдущего значения, так что реальный пример, вероятно, трудно придумать. По крайней мере, мы могли бы создать его без возможного UB, например, читая значение другой расслабленной атомарной переменной, поэтому мы избегаем гонки данных UB, если не видим это значение.
Рассмотрим последовательность:
// broken code where optimization could fix it
memcpy(buf, stuff, sizeof(buf));
done.store(1, mo_relaxed); // relaxed: can reorder with memcpy
done.fetch_add(-1, mo_relaxed);
done.fetch_add(+1, mo_release); // release-store publishes the result
Это может оптимизировать до done.store(1, mo_release);
, что правильно публикует 1
в другом потоке без риска того, что 1
будет виден слишком рано, до обновленных значений buf
.
Но это также могло бы оптимизировать только отмену пары RMW в ограждение после расслабленного магазина, который все еще будет сломан. (И не ошибка оптимизации.)
// still broken
memcpy(buf, stuff, sizeof(buf));
done.store(1, mo_relaxed); // relaxed: can reorder with memcpy
atomic_thread_fence(mo_release);
Я не думал о примере, когда безопасный код нарушается из-за вероятной оптимизации такого рода. Конечно, просто удалив паруполностью, даже когда они seq_cst не всегда безопасны.
Увеличение и уменьшение seq_cst
все еще создает своего рода барьер памяти. Если бы они не были оптимизированы, более ранние хранилища не могли бы чередоваться с более поздними загрузками. Чтобы сохранить это, компиляция для x86, вероятно, все равно должна будет генерировать mfence
.
Конечно, очевидной вещью будет lock add [x], 0
, который на самом деле делает фиктивный RMW общего объекта, который мы сделали x++
/ x--
вкл. Но я думаю одного барьера памяти, не связанного с доступом к этому фактическому объекту или строке кэша, достаточно.
И, конечно, он должен действовать как барьер памяти времени компиляции,блокирование переупорядочения во время компиляции неатомарного и атомарного доступа к нему.
Для acq_rel или более слабого fetch_add(0)
или отмены последовательности, барьер памяти во время выполнения может возникнуть бесплатно на x86, только нужно ограничить компиляциюпорядок времени.
См. также раздел моего ответа по Может ли num ++ быть атомарным для 'int num'? , а также в комментариях к ответу Ричарда Ходжеса. (Но обратите внимание, что некоторые из этих обсуждений смущены рассуждениями о том, когда между ++
и --
происходят изменения в других объектах. Конечно, все упорядочения операций этого потока, подразумеваемые атомами, должны быть сохранены.)
Как я уже сказал, это все гипотетически, и реальные компиляторы не собираются оптимизировать атомарность, пока пыль не осядет на N4455 / P0062.