Почему компиляторы не объединяют избыточные записи std :: atomic?

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

#include <atomic>
std::atomic<int> y(0);
void f() {
  auto order = std::memory_order_relaxed;
  y.store(1, order);
  y.store(1, order);
  y.store(1, order);
}

Каждый компилятор, который я пробовал, выполняет указанную выше запись три раза. Какой законный, свободный от расы наблюдатель может увидеть разницу между приведенным выше кодом и оптимизированной версией с одной записью (т. Е. Не применяется правило «как если»)?

Если переменная была изменчивой, то оптимизация, очевидно, неприменима. Что мешает в моем случае?

Вот код в обозревателе компилятора.


person PeteC    schedule 30.08.2017    source источник
comment
Этот шаг оптимизации, вероятно, не приведет к большому ускорению работы реального приложения по сравнению со стоимостью выполнения шага оптимизации, особенно когда код нетривиальный. Этот доклад в некоторой степени связан.   -  person nwp    schedule 30.08.2017
comment
А что, если f - это только один поток из многих, пишущих в y, в то время как другие читают из y? Если компилятор объединяет записи в одну запись, поведение программы может неожиданно измениться.   -  person Some programmer dude    schedule 30.08.2017
comment
@Someprogrammerdude Раньше такое поведение не было гарантировано, поэтому оптимизация не станет недействительной.   -  person nwp    schedule 30.08.2017
comment
@Someprogrammerdude Я предполагаю такую ​​ситуацию и до сих пор не понимаю. «f () работает очень быстро» - это всегда возможность планирования, поэтому ни одна действующая программа не может предположить, что она может видеть каждую из этих отдельных записей.   -  person PeteC    schedule 30.08.2017
comment
Но вы не знаете, и это одна из проблем. Если мы не знаем всех возможных вариантов использования, как компилятор сможет это сделать?   -  person Some programmer dude    schedule 30.08.2017
comment
очень практичный аргумент: для компилятора было бы трудно рассуждать об избыточности хранилищ в общем случае, в то время как для того, кто пишет код, должно быть тривиально, чтобы избежать таких избыточных записей, так зачем же разработчикам компилятора беспокоиться о том, чтобы добавить такую ​​оптимизацию?   -  person 463035818_is_not_a_number    schedule 30.08.2017
comment
Похоже, ответ здесь может покрыть его.   -  person NathanOliver    schedule 30.08.2017
comment
@NathanOliver Как это связано? Оптимизация компилятора, которая добавляет запись, которая потенциально приводит к гонке данных, совсем не то же самое, что оптимизация, которая удаляет избыточные потокобезопасные записи.   -  person nwp    schedule 30.08.2017
comment
@NathanOliver Спасибо, но удаление двух избыточных хранилищ не приведет к назначению потенциально разделяемой области памяти, которая не будет изменена абстрактной машиной, поэтому я не думаю, что эта часть стандарта помогает.   -  person PeteC    schedule 30.08.2017
comment
Проблема здесь в том, что невозможно доказать, что магазины избыточны. Предположим, что выполняется другой поток, который устанавливает y в 42 между 2-м и 3-м хранилищами, y все равно будет 1 в конце f. Если бы избыточные хранилища были удалены, y будет 2 в конце f.   -  person Richard Critten    schedule 30.08.2017
comment
@RichardCritten Невозможно написать программу на C ++, которая устанавливает y в 42 между 2-м и 3-м хранилищами. Вы можете написать программу, которая просто делает магазин, и, возможно, вам повезет, но нет никакого способа гарантировать это. Невозможно сказать, произошло ли это из-за того, что избыточные записи были удалены, или из-за того, что вам просто не повезло с синхронизацией, следовательно, оптимизация действительна. Даже если это действительно произойдет, у вас нет возможности узнать, потому что это могло произойти до первого, второго или третьего.   -  person nwp    schedule 30.08.2017
comment
Мне действительно приятно слышать, что компилятор не оптимизирует это.   -  person Michaël Roy    schedule 30.08.2017
comment
Стандартный комитет не уверен, всегда ли они согласны с агрессивными атомарными оптимизациями, поэтому компиляторы, вероятно, просто избегают их. См. P0062 для обсуждения некоторых вопросов атомика и агрессивная оптимизация. В документах действительно объясняется, что тип оптимизации, которого вы ожидаете, действительно вполне допустим, но не всегда того, чего ожидает пользователь.   -  person Morwenn    schedule 30.08.2017
comment
Прозаический ответ состоит в том, что, вероятно, никогда не было достаточно кода, который выглядел бы так, чтобы любой оптимизатор-писатель решил потрудиться над написанием оптимизации для него.   -  person TripeHound    schedule 30.08.2017
comment
@Morwenn интересно читать, спасибо. (Хороший кандидат на ответ!)   -  person PeteC    schedule 30.08.2017
comment
@TripeHound: Да. Устройство Даффа настолько непонятно, что никто никогда его не видел.   -  person Eric Towers    schedule 31.08.2017
comment
@TripeHound Не так много кода, который создает избыточные shared_ptr экземпляры во встроенных функциях?   -  person curiousguy    schedule 14.12.2018
comment
@Morwenn Комитет std не уверен, что можно переносить атомарные операции в очень длинные циклы. Похоже, они не задумывались над этой проблемой серьезно, как то же самое можно сказать о нестабильных операциях или даже о регулярном вводе-выводе. (Или даже переместить код вокруг операций синхронизации.)   -  person curiousguy    schedule 09.06.2019


Ответы (9)


Стандарты C ++ 11 / C ++ 14 в том виде, в котором они написаны, позволяют сворачивать / объединять три хранилища в одно хранилище окончательного значения. Даже в таком случае:

  y.store(1, order);
  y.store(2, order);
  y.store(3, order); // inlining + constant-folding could produce this in real code

Стандарт не гарантирует, что наблюдатель, вращающийся на y (с атомарной загрузкой или CAS), когда-либо увидит y == 2. Программа, зависящая от этого, будет иметь ошибку гонки данных, но только гонку типа ошибки разнообразия сада, а не гонку данных типа неопределенного поведения C ++. (Это UB только с неатомарными переменными). Программа, которая ожидает иногда увидеть это, вовсе не обязательно содержит ошибки. (См. Ниже re: индикаторы выполнения.)

Любой порядок, который возможен на абстрактной машине C ++, может быть выбран (во время компиляции) в качестве порядка, который будет всегда. Это правило «как если бы» в действии. В этом случае это как если бы все три хранилища произошли друг за другом в глобальном порядке, без загрузки или сохранения из других потоков между y=1 и y=3.

Это не зависит от целевой архитектуры или оборудования; точно так же, как переупорядочение во время компиляции расслабленных атомарных операций разрешено, даже если ориентированный на строго упорядоченный x86. Компилятор не должен сохранять ничего, что вы могли бы ожидать, думая об оборудовании, для которого вы компилируете, поэтому вам нужны барьеры. Барьеры могут компилироваться в нулевые инструкции asm.


Так почему компиляторы не делают эту оптимизацию?

Это проблема качества реализации, которая может изменить наблюдаемую производительность / поведение на реальном оборудовании.

Самый очевидный случай, когда это проблема, - это индикатор выполнения. Выведение хранилищ из цикла (который не содержит других атомарных операций) и сворачивание их всех в один приведет к тому, что индикатор выполнения останется на 0, а затем перейдет на 100% в самом конце.

В C ++ 11 std::atomic не существует способа запретить это делать в тех случаях, когда вы этого не хотите, поэтому на данный момент компиляторы просто предпочитают никогда не объединять несколько атомарных операций в одну. (Объединение их всех в одну операцию не меняет их порядок относительно друг друга.)

Составители компиляторов правильно заметили, что программисты ожидают, что атомарное хранилище действительно будет происходить в памяти каждый раз, когда источник делает y.store(). (См. Большинство других ответов на этот вопрос, в которых утверждается, что хранилища должны выполняться отдельно, поскольку возможные читатели ожидают увидеть промежуточное значение.) Т.е. это нарушает принцип наименьшего удивления.

Однако в некоторых случаях это было бы очень полезно, например, чтобы избежать бесполезного shared_ptr ref count inc / dec в цикле.

Очевидно, что любое переупорядочение или объединение не может нарушать никаких других правил упорядочивания. Например, num++; num--; по-прежнему должен быть полным барьером для переупорядочения во время выполнения и компиляции, даже если он больше не затрагивает память на num.


Обсуждается возможность расширения std::atomic API, чтобы дать программистам контроль над такой оптимизацией, после чего компиляторы смогут оптимизировать, когда это будет полезно, что может происходить даже в тщательно написанном коде, который не является преднамеренным. неэффективно. Некоторые примеры полезных кейсов для оптимизации упоминаются в следующих ссылках на обсуждения / предложения в рабочих группах:

  • http://wg21.link/n4455: N4455 Ни один нормальный компилятор не оптимизирует атомику
  • http://wg21.link/p0062: WG21 / P0062R1: Когда компиляторам следует оптимизировать атомикс?

См. Также обсуждение этой же темы в ответе Ричарда Ходжеса на Может ли num ++ быть атомарным для 'int num'? (см. Комментарии). См. Также последний раздел моего ответа на тот же вопрос, где я более подробно утверждаю, что такая оптимизация разрешена. (Оставляем это здесь коротким, потому что эти ссылки на рабочую группу C ++ уже подтверждают, что текущий стандарт в том виде, в котором он написан, допускает это, и что текущие компиляторы просто не оптимизируются специально.)


В рамках текущего стандарта volatile atomic<int> y был бы одним из способов гарантировать, что его хранилища не могут быть оптимизированы. (Как указывает Херб Саттер в SO-ответе, volatile и atomic уже разделяют некоторые требования, но они разные). См. Также взаимосвязь std::memory_order с volatile на cppreference.

Доступ к volatile объектам не может быть оптимизирован (например, потому что они могут быть отображенными в память регистрами ввода-вывода).

Использование volatile atomic<T> в основном решает проблему с индикатором выполнения, но это некрасиво и может выглядеть глупо через несколько лет, если / когда C ++ выберет другой синтаксис для управления оптимизацией, чтобы компиляторы могли начать делать это на практике.

Я думаю, мы можем быть уверены, что компиляторы не начнут выполнять эту оптимизацию, пока не появится способ ее контролировать. Надеюсь, это будет своего рода подписка (например, memory_order_release_coalesce), которая не изменит поведение существующего кода C ++ 11/14, когда он скомпилирован как C ++. Но это может быть похоже на предложение в wg21 / p0062: tag not-optimize case with [[brittle_atomic]].

wg21 / p0062 предупреждает, что даже volatile atomic не решает всего, и не рекомендует использовать его для этой цели. Это дает такой пример:

if(x) {
    foo();
    y.store(0);
} else {
    bar();
    y.store(0);  // release a lock before a long-running loop
    for() {...} // loop contains no atomics or volatiles
}
// A compiler can merge the stores into a y.store(0) here.

Даже с volatile atomic<int> y компилятору разрешено вытеснить y.store() из if/else и просто сделать это один раз, потому что он по-прежнему выполняет ровно 1 хранилище с тем же значением. (Что будет после длинного цикла в ветке else). Особенно если в магазине всего relaxed или release вместо seq_cst.

volatile действительно останавливает объединение, обсуждаемое в вопросе, но это указывает на то, что другие оптимизации на atomic<> также могут быть проблематичными для реальной производительности.


Другие причины отказа от оптимизации включают: никто не написал сложный код, который позволил бы компилятору безопасно выполнять эту оптимизацию (ни разу не ошибаясь). Этого недостаточно, потому что N4455 утверждает, что LLVM уже реализует или может легко реализовать некоторые из упомянутых оптимизаций.

Причина, по которой программистов сбивает с толку, безусловно, правдоподобна. Код без блокировок достаточно сложно правильно написать.

Не будьте случайны в использовании атомного оружия: оно недешево и мало оптимизирует (в настоящее время совсем не оптимизирует). Однако не всегда легко избежать избыточных атомарных операций с std::shared_ptr<T>, поскольку не существует его неатомарной версии (хотя один из ответов здесь дает простой способ определить shared_ptr_unsynchronized<T> для gcc).

person Peter Cordes    schedule 30.08.2017
comment
Таким образом, оптимизация (а) правильная, (б) сложная и (в) неожиданная. WG21 / P0062R1 особенно интересен. Я лично не считаю, что принцип наименьшего удивления является здесь золотым правилом; на этом уровне языка вам необходимо знать правила, прежде чем играть в игру. Спасибо, что помогли мне понять. - person PeteC; 31.08.2017
comment
@PeteC: Да, я думаю, важно понимать, что оптимизация разрешена, и что ее невыполнение является проблемой QOI, а не проблемой соответствия стандартам, и что что-то может измениться в будущем стандарте. - person Peter Cordes; 31.08.2017
comment
Таким образом, C ++ 11/14 могут некорректно взаимодействовать с программным вводом-выводом, написанным в стиле Устройство Даффа. Большой. - person Eric Towers; 31.08.2017
comment
@EricTowers нет, в устройстве Даффа выходной регистр обязательно будет объявлен изменчивым (это учебный случай для volatile), и выход будет таким, как ожидалось. - person PeteC; 31.08.2017
comment
@PeteC: Учитывая диапазон целей, для которых используются такие языки, как C и C ++, программам для некоторых целей и полей приложений часто требуется семантика, которая поддерживается не везде; сам язык поднимает вопрос о том, когда они должны поддерживаться как проблема QoI, но если программисты в определенной области найдут поведение удивительным, это довольно хороший признак того, что качественные реализации в этой области не должны вести себя таким образом, если явно не запрашивается . Сами языковые правила недостаточно полны, чтобы сделать язык полезным для всех целей без POLA. - person supercat; 18.07.2018
comment
и злоупотребляет значением volatile, volatile int w; w=0; delay_loop(1000); w=0; так же, lol - person curiousguy; 08.12.2018
comment
@curiousguy: Я решил перефразировать это, но вы, вероятно, будете использовать delay_loop только в непереносимом коде, ориентированном на конкретную встроенную систему. Во многих случаях volatile во встроенном программировании используется там, где, возможно, более подходящими были бы расслабленные или атомарные acq / rel. (По крайней мере, если вы также включите static_assert, что он не блокируется.) - person Peter Cordes; 08.12.2018
comment
В любом случае не чувствуется, что какой-либо компилятор должен пытаться агрессивно оптимизировать код, проталкивая вычисления через volatile-доступ, даже если он строго соответствует букве спецификации: volatile следует расплывчато упорядочить WRT чистый код вокруг него. И, по крайней мере, люди, утверждающие, что оптимизация изменчивого доступа Java / атомарного доступа C ++ является дурным тоном, должны обнаружить, что оптимизация для изменяемого доступа C / C ++ не менее плоха. - person curiousguy; 08.12.2018
comment
@curiousguy: согласен, качественные реализации, вероятно, не будут переупорядочивать volatile из-за дорогостоящих вычислений, даже если у них есть соблазн сделать это из-за общего хвоста в обеих ветвях. Но стандарт допускает поведение, которое нам не нужно, поэтому, по крайней мере, комитет по стандартам должен попытаться улучшить. Вы можете просто оставить все как есть и сказать, что уже возможно создать строго соответствующую реализацию C ++, которая почти бесполезна для низкоуровневого системного программирования, но во многом это происходит за счет нарушения предположений, которые делает большинство кода, например, целочисленные типы. t иметь набивку. Не оптимизация. - person Peter Cordes; 08.12.2018
comment
позволить компилятору безопасно выполнять эту оптимизацию (ни разу не ошибаясь) Обнаружение вычисления ограниченной стоимости тривиально (любой код без цикла или goto и без вызова схемы забавы тривиален); объединение избыточных атомарных операций, происходящих только с тривиальным кодом стоимости между ними, кажется тривиальным. Я полагаю, это справится с некоторым расслабленным incr в стиле shared_ptr с последующим указанием выпуска. - person curiousguy; 08.04.2019

Вы имеете в виду устранение мертвых запасов.

Не запрещено устранять атомарный мертвый накопитель, но сложнее доказать, что атомарный накопитель квалифицируется как таковой.

Традиционные оптимизации компилятора, такие как устранение мертвого хранилища, могут выполняться для атомарных операций, даже для последовательно согласованных.
Оптимизаторы должны быть осторожны, чтобы не делать этого в точках синхронизации, поскольку другой поток выполнения может наблюдать или изменять память, что означает, что традиционные оптимизации должны учитывать больше промежуточных инструкций, чем обычно при рассмотрении оптимизации атомарных операций.
В случае исключения мертвого хранилища недостаточно доказать, что атомарное хранилище пост-доминирует и использует псевдоним другого, чтобы исключить другое хранилище.

из N4455 Никакой разумный компилятор не оптимизирует атомику

Проблема атомарной DSE, в общем случае, заключается в том, что она включает поиск точек синхронизации, в моем понимании этот термин означает точки в коде, где существует связь случиться-до между инструкциями в потоке. A и инструкции для другого потока B.

Рассмотрим этот код, выполняемый потоком A:

y.store(1, std::memory_order_seq_cst);
y.store(2, std::memory_order_seq_cst);
y.store(3, std::memory_order_seq_cst);

Можно ли его оптимизировать как y.store(3, std::memory_order_seq_cst)?

Если поток B ожидает увидеть y = 2 (например, с CAS), он никогда не заметит этого, если код будет оптимизирован.

Однако, насколько я понимаю, наличие цикла B и CASing на y = 2 - это гонка данных, поскольку нет полного порядка между инструкциями двух потоков.
Выполнение, при котором инструкции A выполняются до того, как цикл B становится наблюдаемым (т. Е. разрешено) и, таким образом, компилятор может оптимизировать до y.store(3, std::memory_order_seq_cst).

Если потоки A и B каким-то образом синхронизируются между хранилищами в потоке A, тогда оптимизация не будет разрешена (будет индуцирован частичный порядок, что, возможно, приведет к тому, что B потенциально будет наблюдать y = 2).

Доказать, что такой синхронизации нет, сложно, так как это требует рассмотрения более широкой области и учета всех особенностей архитектуры.

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

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

person Margaret Bloom    schedule 30.08.2017
comment
Вы имеете в виду N4455, но, похоже, у вас совершенно другая интерпретация N4455, чем у меня. Даже первый пример в N4455 более сложен, чем ваш пример (добавляет вместо прямого сохранения), и этот пример описан как неконфликтный (возможна оптимизация). И учитывая, что N4455 также заявляет, что LLVM реализует некоторые из упомянутых оптимизаций, можно с уверенностью предположить, что самая простая из них определенно реализована. - person MSalters; 30.08.2017
comment
@MSalters. Хотя, честно говоря, N4455 был черновиком, только одна оптимизация указана как реализованная (Я не смог воспроизвести это). Я считаю, что первый пример на самом деле не отличается от моего: оба должны быть оптимизируемыми, но это не так. Однако, хотя у меня есть понимание того, как это работает под капотом, я недостаточно хорошо разбираюсь в стандарте C ++. Конечно, ваше понимание лучше моего! Я никогда не хотел бы распространять дезинформацию, если вы видите в этом ответе неустранимую ошибку, пожалуйста, дайте мне знать! - person Margaret Bloom; 31.08.2017
comment
Хм, может, нужно немного почитать, что там происходит. Насчет того, что N4455 - черновик: суть не в этом; это дает нам взгляд изнутри с точки зрения разработчиков компилятора. Это также означает, что они играют с кодовой базой, которой у нас еще нет;) - person MSalters; 31.08.2017
comment
Зацикливание потока на CAS или атомарная загрузка, ожидающая увидеть y=2, является условием гонки, но просто обычным типом ошибки, а не типом неопределенного поведения (потому что он относится к типу atomic). - person Peter Cordes; 31.08.2017
comment
@MSalters: Насколько я понимаю, компиляторы могут оптимизировать, но пока предпочитают не делать этого, потому что это нарушит ожидания программистов в отношении таких вещей, как индикатор выполнения. Чтобы программисты могли выбирать, нужен новый синтаксис. Стандарт в том виде, в каком он написан, позволяет выбирать (во время компиляции) любое возможное переупорядочение, которое может произойти на абстрактной машине C ++, в качестве порядка, который выполняется всегда, но это нежелательно. См. Также wg21.link/p0062. - person Peter Cordes; 31.08.2017
comment
@MSalters: опубликовал мой собственный ответ, поскольку там так много неправильных ответов на этот вопрос. (Если только не Я ошибаюсь.) Этот ответ верен в отношении центрального вопроса, но причина отказа от оптимизации заключается в том, что компиляторы намеренно не являются проблемой качества реализации, даже в тех случаях, когда где они могли бы легко обнаружить оптимизацию. - person Peter Cordes; 31.08.2017
comment
@MargaretBloom: 1) последовательно последовательное и расслабленное не имеет значения (разница актуальна только тогда, когда в игру вступают другие участки памяти). 2) В вашем примере проверки y==2 есть то, что я называю логической гонкой, но не гонкой за данные. Это очень важное различие. Подумайте о неопределенном и неопределенном поведении: может когда-нибудь увидеть y==2, а может и нет, но никаких назальных демонов. 3) Существует всегда полный порядок операций с одним атомаром (даже с relaxed). Порядок может быть просто непредсказуемым. 4) Я согласен с тем, что атомикс может сбивать с толку. ;-) - person Arne Vogel; 31.08.2017
comment
Это много слов и не упоминается тот факт, что стандарт гарантирует, что каждая запись будет видна в других потоках. Поскольку оборудование не может угадать, что происходит в других потоках, это исключает любую оптимизацию записи. - person Michaël Roy; 05.09.2017
comment
@ MichaëlRoy Я считаю, что программа должна вести себя так, как если бы каждая запись была видна другому потоку, если так сказано в стандарте. Это оставляет место для оптимизации. - person Margaret Bloom; 05.09.2017
comment
«Как будто» - это не гарантия. Запись должна выполняться каждый раз, поскольку в конечном итоге гарантия лежит на оборудовании. Потоки и ядра не поддерживают ESP. - person Michaël Roy; 05.09.2017
comment
@ MichaëlRoy стандарт гарантирует, что каждая запись будет видна в других потоках Эта гарантия не имеет никакого смысла. - person curiousguy; 08.12.2018
comment
атомарные переменные объявлены volatile, поэтому компилятор не может выполнить оптимизацию, обсуждаемую в вопросе, как определено в стандарте C ++. Это одна часть гарантии, другая часть гарантии предоставляется процессором через его атомарные инструкции. - person Michaël Roy; 29.12.2018

Пока вы меняете значение атома в одном потоке, другой поток может проверять его и выполнять операцию на основе значения атома. Приведенный вами пример настолько конкретен, что разработчики компилятора не видят в нем смысла оптимизировать. Однако, если один поток устанавливает, например. последовательные значения для атомарного: 0, 1, 2 и т. д., другой поток может помещать что-то в слоты, указанные значением атомарного.

person Serge Rogatch    schedule 30.08.2017
comment
Примером этого может быть индикатор выполнения, который получает текущее состояние от atomic, в то время как рабочий поток выполняет некоторую работу и обновляет atomic без другой синхронизации. Оптимизация позволит компилятору просто записать 100% один раз и не выполнять избыточные записи, из-за чего индикатор выполнения не показывает прогресс. Спорный вопрос, следует ли допускать такую ​​оптимизацию. - person nwp; 30.08.2017
comment
Возможно, этот пример возник не дословно, а только после множества оптимизаций, таких как встраивание и распространение констант. В любом случае, вы говорите, что их можно объединить, но не стоит ли беспокоиться? - person Deduplicator; 30.08.2017
comment
@nwp: Стандарт в том виде, в каком он написан, допускает это позволяет. Любое переупорядочение, возможное на абстрактной машине C ++, может быть выбрано во время компиляции, как то, что происходит всегда. Это нарушает ожидания программиста в отношении таких вещей, как индикаторы выполнения (вывод атомарных хранилищ из цикла, который не касается каких-либо других атомарных переменных, потому что одновременный доступ к неатомарным переменным - это UB). На данный момент компиляторы предпочитают не оптимизировать, хотя могли. Надеюсь, появится новый синтаксис для управления, когда это будет разрешено. wg21.link/p0062 и wg21.link/n4455. - person Peter Cordes; 31.08.2017

NB: Я собирался прокомментировать это, но это слишком многословно.

Один интересный факт заключается в том, что в терминах C ++ такое поведение не является гонкой за данные.

Примечание 21 на стр.14 представляет интерес: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3690.pdf (выделено мной):

Выполнение программы содержит гонку данных, если она содержит два конфликтующих действия в разных потоках, по крайней мере одно из которых не является атомарным.

Также на стр.11 примечание 5:

«Расслабленные» атомарные операции не являются операциями синхронизации, хотя, как и операции синхронизации, они не могут участвовать в гонке данных.

Таким образом, конфликтующее действие над атомаром никогда не является гонкой за данные - с точки зрения стандарта C ++.

Все эти операции атомарны (и особенно расслаблены), но никакой гонки данных здесь, ребята!

Я согласен, что нет надежной / предсказуемой разницы между этими двумя на любой (разумной) платформе:

include <atomic>
std::atomic<int> y(0);
void f() {
  auto order = std::memory_order_relaxed;
  y.store(1, order);
  y.store(1, order);
  y.store(1, order);
}

а также

include <atomic>
std::atomic<int> y(0);
void f() {
  auto order = std::memory_order_relaxed;
  y.store(1, order);
}

Но в рамках определения модели памяти C ++ это не гонка за данными.

Я не могу легко понять, почему предоставляется это определение, но оно дает разработчику несколько карточек для бессистемного взаимодействия между потоками, которые, как они могут знать (на их платформе), будут статистически работать.

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

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

Короче говоря, стандарт, по-видимому, указывает, что там, где другие потоки могут видеть эффект «молотка» от значения, установленного 3 раза, другие потоки должны иметь возможность видеть этот эффект (даже если они иногда не могут!). Это тот случай, когда практически все современные платформы, которые другие потоки могут при некоторых обстоятельствах столкнуться с проблемой.

person Persixty    schedule 30.08.2017
comment
Никто не сказал, что это гонка данных - person LWimsey; 30.08.2017
comment
@LWimsey Действительно, и это не гонка за данными. В этом-то и дело. Стандарт C ++ занимается гонками данных. Так что рассуждения о наблюдателях без расы в ОП неуместны. В C ++ нет проблем с наблюдателями, открытыми для гонок, да и с такими вещами, как try_lock_for пригласить гонки! Ответ на вопрос, почему компиляторы не оптимизируют это, заключается в том, что он определил семантику (гоночную или другую), и стандарт хочет, чтобы это произошло (какими бы они ни были). - person Persixty; 30.08.2017
comment
Вращение атомарной нагрузки y в поисках y==2 - это состояние гонки (и, вероятно, это то, что имел в виду OP, говоря о наблюдателе без гонки). Это всего лишь гонка за ошибками в саду, а не с неопределенным поведением C ++. - person Peter Cordes; 31.08.2017

Короче говоря, потому что стандарт (например, парагарафы около и ниже 20 в [intro.multithread]) не допускает этого.

Существуют гарантии «случилось раньше», которые должны быть выполнены и которые, среди прочего, исключают переупорядочивание или объединение операций записи (в параграфе 19 это прямо говорится даже о переупорядочении).

Если ваш поток записывает в память три значения (скажем, 1, 2 и 3) одно за другим, другой поток может прочитать это значение. Если, например, ваш поток прерывается (или даже если он выполняется одновременно) и другой поток также записывает в это место, тогда наблюдающий поток должен видеть операции в том же порядке, в котором они происходят ( либо по расписанию, либо по совпадению, либо по любой другой причине). Это гарантия.

Как это возможно, если вы выполняете только половину операций записи (или даже только одну)? Это не так.

Что, если ваш поток вместо этого записывает 1 -1 -1, а другой спорадически записывает 2 или 3? Что, если третий поток наблюдает за местоположением и ждет определенного значения, которое никогда не появляется, потому что оно оптимизировано?

Невозможно предоставить гарантии, которые даются, если операции сохранения (и загрузки тоже) не выполняются должным образом. Все они и в том же порядке.

person Damon    schedule 30.08.2017
comment
Оптимизация не нарушает гарантии того, что произойдет раньше. В другом примере они могут быть, но не в этом. Очевидно, что можно предоставить гарантии для примера OP. Ничего не переупорядочивается, поэтому эта часть не имеет отношения к вопросу. - person nwp; 30.08.2017
comment
@Damon Не могли бы вы уточнить, какие части текста запрещают эту оптимизацию? - person LWimsey; 30.08.2017
comment
@nwp вообще разобраться не возможно. Этот пример до глупости тривиален. Если программист знает, что написать его трижды - это то же самое, что написать его один раз, то ему следовало написать его только один раз. - person OrangeDog; 30.08.2017
comment
@OrangeDog Так что дословно это вряд ли появится. Хотя это могло быть результатом постоянного распространения, встраивания и любого количества других оптимизаций. - person Deduplicator; 30.08.2017
comment
Вы говорите, что есть что-то, запрещающее объединение записи в [intro.multithread]. Процитируйте его. Я не могу найти это. - person Deduplicator; 30.08.2017
comment
Это просто выдумка. - person T.C.; 31.08.2017
comment
@Deduplicator: не существует такого языка, который гарантирует, что другие потоки иногда должны видеть промежуточные значения из последовательности записей в другом потоке. Тот факт, что компиляторы избегают такой оптимизации, является проблемой качества реализации, пока комитет по стандартам C ++ не добавит способ разрешить ее выборочно, потому что это может быть проблемой. См. мой ответ для некоторых ссылок на рабочие стандарты. предложения группы, которые подтверждают эту интерпретацию, что это разрешено. - person Peter Cordes; 31.08.2017
comment
@Deduplicator: абзацы, которые я процитировал, делают это в своей сущности. Как указано, переупорядочение явно запрещено (19), тогда как объединение не рассматривается явно. Однако это фактически запрещено, потому что, например, 15 и 17 не могут быть выполнены, за исключением самых надуманных тривиальных примеров, которые не содержат запись-запись или чтение-запись, или запись-чтение вообще (или, ну, без параллелизма). Например, нет способа, чтобы A был раньше в порядке модификации (по сравнению, например, с B), если вы оптимизируете A. - person Damon; 31.08.2017
comment
@Deduplicator Все это будет результатом столь же тривиального и бессмысленного кода. - person OrangeDog; 31.08.2017
comment
@PeterCordes Не существует такого языка, который гарантирует, что другие потоки иногда должны видеть промежуточные значения из последовательности записей в другом потоке. Хм? Многие языки имеют это, обычно через volatile или явные точки синхронизации памяти. - person OrangeDog; 31.08.2017
comment
@OrangeDog: Я имел в виду не volatile atomic. Я думаю, вы правы, что volatile atomic переносимо отключит такую ​​оптимизацию, что иногда делает это возможным на типичном текущем оборудовании SMP. (Как и барьер памяти компилятора, такой как GNU C asm(""::: "memory").) Но стандарт C ++ по-прежнему не гарантирует, что поток, вращающийся на y, когда-либо увидит y==2. например на однопроцессорной машине это очень маловероятно, а детерминированные переключатели контекста или что-то еще могут сделать это невозможным. - person Peter Cordes; 31.08.2017
comment
@PeterCordes Я не смотрел, но полагаю, что различные из этих стандартных библиотечных элементов (например, std::atomic) определяют барьеры памяти (или эквивалентную семантику), если они поддерживаются целевой системой. Конечно, они есть на других языках. - person OrangeDog; 31.08.2017
comment
@OrangeDog: обычно атомики упорядочиваются только по отношению к другим атомам, потому что одновременный доступ к переменным, отличным от atomic, по-прежнему является UB (гонка данных). atomic_signal_fence заказывает даже обычные переменные в gcc, но IDK, если это деталь реализации или требуется стандартом. См. stackoverflow.com/questions/40579342/ для atomic_thread_fence по сравнению с atomic_signal_fence в gcc. - person Peter Cordes; 31.08.2017
comment
@PeterCordes, который ничего не меняет. Единственный способ прочитать запись в атомарный объект - через атомарный. - person OrangeDog; 31.08.2017
comment
@OrangeDog: это объясняет, почему atomic_thread_fence или хранилище релизов не эквивалентно asm("" ::: "memory"): потому что барьер компилятора для неатомических элементов блокирует оптимизацию (неатомарных операций в том же цикле или что-то еще) что стандарт не требует его блокирования. - person Peter Cordes; 31.08.2017
comment
@PeterCordes Не существует такого языка, который гарантирует, что другие потоки иногда должны видеть промежуточные значения из последовательности записей в другом потоке. вот что я опровергаю. Все остальное неактуальная деталь. - person OrangeDog; 31.08.2017
comment
@OrangeDog: Я понимаю, о чем вы сейчас говорите. Все, что я пытался заявить, это то, что y.store(1, mo_relaxed) не подразумевает какого-либо барьера, а не то, что вы не можете получить барьер (особенно с языковыми расширениями, такими как GNU C ++). Чтобы прояснить, вы имели в виду барьер переупорядочения, такой как x86 mfence? std::atomic позволяет получить такой барьер. Но std::atomic не имеет содержимого памяти, должны быть постоянные барьеры оптимизации; это деталь реализации, которую C ++ 11 не определяет, потому что она имеет значение только для таких вещей, как встроенный asm, который выходит за рамки чистого ISO C ++. - person Peter Cordes; 31.08.2017
comment
Если std::atomic не работает, значит, он так же сломан, как volatile в Java 1.3. - person OrangeDog; 31.08.2017
comment
@OrangeDog Как атомный сломался? Для чего использовать? - person curiousguy; 08.12.2018

Практический вариант использования шаблона, если поток делает что-то важное между обновлениями, которое не зависит от y и не изменяет его, может быть следующим: * Поток 2 считывает значение y, чтобы проверить, насколько продвинулся поток 1.

Итак, возможно, поток 1 должен загрузить файл конфигурации как шаг 1, поместить его проанализированное содержимое в структуру данных как шаг 2 и отобразить главное окно как шаг 3, в то время как поток 2 ожидает выполнения шага 2, чтобы он мог параллельно выполнять другую задачу, которая зависит от структуры данных. (Конечно, этот пример требует семантики получения / выпуска, а не упрощенного порядка.)

Я почти уверен, что соответствующая реализация позволяет потоку 1 не обновлять y на любом промежуточном этапе - хотя я не вдавался в подробности языкового стандарта, я был бы шокирован, если бы он не поддерживает оборудование, на котором другой поток, опрашивающий y, может никогда не увидеть значение 2.

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

person Davislor    schedule 30.08.2017
comment
Да, стандарт позволяет это, но настоящие компиляторы не выполняют эту оптимизацию, потому что нет синтаксиса для их остановки в таких случаях, как обновление индикатора выполнения, поэтому это проблема качества реализации. См. мой ответ - person Peter Cordes; 31.08.2017
comment
@PeterCordes Хороший ответ, особенно ссылки на текущие обсуждения WG. - person Davislor; 31.08.2017

Давайте немного отойдем от патологического случая, когда три магазина находятся непосредственно рядом друг с другом. Предположим, между хранилищами выполняется некоторая нетривиальная работа, и эта работа вообще не включает y (так что анализ пути к данным может определить, что три хранилища на самом деле избыточны, по крайней мере, в этом потоке), и сам по себе не создает каких-либо барьеров для памяти (чтобы что-то еще не заставляло хранилища быть видимыми для других потоков). Теперь вполне возможно, что у других потоков есть возможность выполнять работу между хранилищами, и, возможно, эти другие потоки манипулируют y, и что у этого потока есть какая-то причина, по которой ему нужно сбросить его на 1 (2-е хранилище). Если бы первые два магазина были отброшены, это изменило бы поведение.

person Andre Kostur    schedule 30.08.2017
comment
Гарантировано ли измененное поведение? Оптимизации все время меняют поведение, они, как правило, ускоряют выполнение, что может иметь огромное влияние на чувствительный ко времени код, но это считается допустимым. - person nwp; 30.08.2017
comment
Атомарная часть меняет вещи. Это заставляет магазин быть видимым для других потоков. Есть три хранилища для y, которые должны быть видны другим потокам. Если y не были атомарными, тогда, конечно, оптимизатор может отбросить первые два назначения, поскольку ничто в этом потоке не могло видеть, что они были отброшены, и ничто не гарантировало, что назначения будут видны другим потокам. Но поскольку он атомарен и гарантирует, что изменение будет видно другим потокам, оптимизатор не может отбросить этот код. (Не без подтверждения того, что везде else тоже не использует его.) - person Andre Kostur; 30.08.2017
comment
Но 1 запись уже делает его видимым для других потоков. Как другие потоки определят разницу между 1 и 3 записями? - person nwp; 30.08.2017
comment
Смотрите мой полный ответ. Отойдите от патологического случая, когда три магазина буквально рядом друг с другом. Поместите туда некоторый нетривиальный код, чтобы повысить вероятность того, что другие потоки получат некоторые временные интервалы между этими хранилищами. Имейте второй поток, который все, что он делает, это читает y, если y == 1, сделайте что-нибудь (что намного быстрее, чем дополнительная работа, которую мы добавили к первому потоку) и установите его на 2. Этот второй поток должен запускаться 3 раза. , а не один раз в конце. - person Andre Kostur; 30.08.2017
comment
@AndreKostur 'должно быть'? Если вы полагаетесь на это, логика вашей программы нарушена. Задача оптимизатора - получить корректный результат с меньшими усилиями. «У потока 2 нет временных интервалов между хранилищами» - вполне допустимый результат. - person PeteC; 30.08.2017
comment
@PeteC Итак, поток 2 (и 3, и 4 ...) получает много временных отрезков между хранилищами, и поскольку это атомарное хранилище, оно должно быть видимым для других потоков. Оптимизатору не разрешается оптимизировать этот видимый побочный эффект, поскольку он должен предполагать, что что-то может перемещаться между этими двумя хранилищами и может пойти и создать свое собственное хранилище. - person Andre Kostur; 30.08.2017
comment
Стандарт в том виде, в котором он написан, делает позволяет компиляторам оптимизировать окно для другого потока, чтобы что-то сделать. Ваши аргументы в пользу этого (и прочего, вроде индикатора выполнения) заключаются в том, почему настоящие компиляторы предпочитают не выполнять такую ​​оптимизацию. См. мой ответ для некоторых ссылок на обсуждения стандартов C ++. о том, чтобы дать программистам контроль, чтобы можно было проводить оптимизацию там, где это полезно, и избегать там, где это вредно. - person Peter Cordes; 31.08.2017
comment
@AndreKostur Поместите туда нетривиальный код какой код? Код перебирает числа? синхронизацию потоков? делать ввод / вывод? - person curiousguy; 08.12.2018

Писатель компилятора не может просто выполнить оптимизацию. Они также должны убедить себя, что оптимизация допустима в ситуациях, когда автор компилятора намеревается ее применить, что она не будет применяться в ситуациях, когда она недействительна, что она не нарушает код, который на самом деле сломан, но " работает "на других реализациях. Это, наверное, больше работы, чем сама оптимизация.

С другой стороны, я мог себе представить, что на практике (то есть в программах, которые должны выполнять работу, а не в тестах производительности), эта оптимизация очень мало сэкономит время выполнения.

Таким образом, разработчик компилятора рассмотрит стоимость, затем рассмотрит преимущества и риски и, вероятно, решит отказаться от этого.

person gnasher729    schedule 30.08.2017

Поскольку ожидается, что переменные, содержащиеся в объекте std :: atomic, будут доступны из нескольких потоков, следует ожидать, что они будут вести себя, как минимум, так, как если бы они были объявлены с помощью ключевого слова volatile.

Это было стандартной и рекомендованной практикой до того, как в архитектурах ЦП появились строки кэша и т. Д.

[EDIT2] Можно утверждать, что std :: atomic ‹> - это volatile переменные многоядерного возраста. Как определено в C / C ++, volatile достаточно хороши только для синхронизации атомарных чтений из одного потока, когда ISR изменяет переменную (которая в данном случае фактически является атомарной записью, как видно из основного потока. ).

Я лично рад, что ни один компилятор не сможет оптимизировать запись в атомарную переменную. Если запись оптимизирована, как вы можете гарантировать, что каждая из этих записей потенциально может быть увидена читателями в других потоках? Не забывайте, что это также часть контракта std :: atomic ‹>.

Рассмотрим этот фрагмент кода, на результат которого сильно повлияет дикая оптимизация компилятором.

#include <atomic>
#include <thread>

static const int N{ 1000000 };
std::atomic<int> flag{1};
std::atomic<bool> do_run { true };

void write_1()
{
    while (do_run.load())
    {
        flag = 1; flag = 1; flag = 1; flag = 1;
        flag = 1; flag = 1; flag = 1; flag = 1;
        flag = 1; flag = 1; flag = 1; flag = 1;
        flag = 1; flag = 1; flag = 1; flag = 1;
    }
}

void write_0()
{
    while (do_run.load())
    {
        flag = -1; flag = -1; flag = -1; flag = -1;
    }
}


int main(int argc, char** argv) 
{
    int counter{};
    std::thread t0(&write_0);
    std::thread t1(&write_1);

    for (int i = 0; i < N; ++i)
    {
        counter += flag;
        std::this_thread::yield();
    }

    do_run = false;

    t0.join();
    t1.join();

    return counter;
}

[РЕДАКТИРОВАТЬ] Сначала я не утверждал, что volatile был центральным элементом реализации атомики, но ...

Поскольку, казалось, были сомнения относительно того, имеет ли volatile какое-либо отношение к атомной энергии, я исследовал этот вопрос. Вот атомарная реализация из VS2017 stl. Как я и предполагал, ключевое слово volatile есть везде.

// from file atomic, line 264...

        // TEMPLATE CLASS _Atomic_impl
template<unsigned _Bytes>
    struct _Atomic_impl
    {   // struct for managing locks around operations on atomic types
    typedef _Uint1_t _My_int;   // "1 byte" means "no alignment required"

    constexpr _Atomic_impl() _NOEXCEPT
        : _My_flag(0)
        {   // default constructor
        }

    bool _Is_lock_free() const volatile
        {   // operations that use locks are not lock-free
        return (false);
        }

    void _Store(void *_Tgt, const void *_Src, memory_order _Order) volatile
        {   // lock and store
        _Atomic_copy(&_My_flag, _Bytes, _Tgt, _Src, _Order);
        }

    void _Load(void *_Tgt, const void *_Src,
        memory_order _Order) const volatile
        {   // lock and load
        _Atomic_copy(&_My_flag, _Bytes, _Tgt, _Src, _Order);
        }

    void _Exchange(void *_Left, void *_Right, memory_order _Order) volatile
        {   // lock and exchange
        _Atomic_exchange(&_My_flag, _Bytes, _Left, _Right, _Order);
        }

    bool _Compare_exchange_weak(
        void *_Tgt, void *_Exp, const void *_Value,
        memory_order _Order1, memory_order _Order2) volatile
        {   // lock and compare/exchange
        return (_Atomic_compare_exchange_weak(
            &_My_flag, _Bytes, _Tgt, _Exp, _Value, _Order1, _Order2));
        }

    bool _Compare_exchange_strong(
        void *_Tgt, void *_Exp, const void *_Value,
        memory_order _Order1, memory_order _Order2) volatile
        {   // lock and compare/exchange
        return (_Atomic_compare_exchange_strong(
            &_My_flag, _Bytes, _Tgt, _Exp, _Value, _Order1, _Order2));
        }

private:
    mutable _Atomic_flag_t _My_flag;
    };

Все специализации в MS stl используют volatile для ключевых функций.

Вот объявление одной из таких ключевых функций:

 inline int _Atomic_compare_exchange_strong_8(volatile _Uint8_t *_Tgt, _Uint8_t *_Exp, _Uint8_t _Value, memory_order _Order1, memory_order _Order2)

Вы заметите, что требуемый volatile uint8_t* содержит значение, содержащееся в std :: atomic. Этот шаблон можно наблюдать во всей реализации MS std :: atomic ‹>. Нет причин для команды gcc или любого другого поставщика stl делать это иначе.

person Michaël Roy    schedule 30.08.2017
comment
volatile не имеет ничего общего с атомикой - person login_not_failed; 30.08.2017
comment
@login_not_failed Но volatile имеет много общего с тем, чтобы не оптимизировать доступ к памяти, что является одним из следствий использования атомики. Atomics добавляет к этому некоторые действительно важные гарантии (атомарность и порядок), но не оптимизируйте это далеко! семантика применима к обоим. - person cmaster - reinstate monica; 30.08.2017
comment
Я не сказал, что они были объявлены с помощью volatile - я подозреваю, что некоторые реализации действительно это делают. Но этот атомикс должен вести себя, по крайней мере, аналогичным образом. - person Michaël Roy; 30.08.2017
comment
@login_not_failed: У меня была такая же первоначальная реакция, но я считаю, что упоминание volatile здесь правильно. Неудивительно, что компиляторы обрабатывают std::atomic переменные иначе, чем обычные переменные, так же, как они обрабатывают volatile переменные по-разному, и по схожим причинам: в обоих случаях что-то необычное делает возможным неожиданное изменение значения переменной, поэтому компилятор должен фактически читать и записывать в переменную вместо кэширования предыдущих операций чтения / записи. - person Max Lybbert; 30.08.2017
comment
Но это неправильно. volatile делает то, чего не atomics, в частности volatile предполагает, что вы разговариваете не с памятью, а с устройствами, где запись 1, 2, 3 может быть последовательностью запуска, которая должна поступать именно так, а чтение этого местоположения может дать вам текущее температура. atomic предполагает, что вы используете обычную память в том месте, где читали то, что написали в последний раз. - person nwp; 30.08.2017
comment
volatile ничего не делает, кроме отключения некоторых оптимизаций. И модификатор volatile прикреплен к переменной, которая действительно применяется к чтению и записи в память. Как язык C ++ не знает об устройствах, и в стандарте устройства не упоминаются. Модификатор volatile также может быть присоединен к части ассемблерного кода, как определено по крайней мере некоторые реализации атомарного ‹›. - person Michaël Roy; 30.08.2017
comment
@ MichaëlRoy, вы неправильно поняли использование квалификатора метода volatile. См. stackoverflow.com/questions/16746070 - person PeteC; 30.08.2017
comment
@PeteC. Конечно же нет. И команда MS STL тоже. Вот как обычно специализируются атомарные операции: inline int _Atomic_compare_exchange_strong_8(volatile _Uint8_t *_Tgt, _Uint8_t *_Exp, _Uint8_t _Value, memory_order _Order1, memory_order _Order2) Вы заметите, что этой функции требуется указатель на изменчивую переменную. Все специализации такие. - person Michaël Roy; 30.08.2017
comment
@ MichaëlRoy, эта цитата не была частью фрагмента STL в вашем ответе. Конечно, вы можете атомарно работать с изменчивым объектом. Это не означает, что атомы летучие или летучие атомы. Ваше утверждение о том, что следует ожидать, что [атомики] будут вести себя как минимум так, как если бы они были объявлены с ключевым словом volatile, совершенно неверно. - person PeteC; 30.08.2017
comment
Эта цитата прямо из MS VS2017 stl. версия 14.10.25017. файл 'xatomic.h', строка 2028. Я уверен, что не собираюсь вставлять всю атомарную реализацию stl. Посмотрите на код в используемом вами stl, и вы увидите, что атомарная переменная в какой-то момент получит изменчивый статус. - person Michaël Roy; 30.08.2017
comment
@PeteC Ключевое слово volatile было введено в C для обозначения переменных, которые могут быть изменены изнутри ISR и прочитаны из основного потока программы. Это поведение очень похоже на переменную, которую можно изменить из другого потока. Это та самая черта, которую можно ожидать от atomic ‹›. Другого практического применения они не имеют. Атомика - изменчивые переменные в эпоху многоядерных процессоров. - person Michaël Roy; 30.08.2017
comment
@ MichaëlRoy, пожалуйста, прочтите stackoverflow.com/questions/8819095, я не могу улучшить ответы Энтони Уильямса и Джеймса Канце. - person PeteC; 30.08.2017
comment
Я не могу улучшить свою. std :: atomic ‹› - изменчивые переменные. И единственная причина для существования - обеспечить функциональность volatile на многоядерных процессорах. - person Michaël Roy; 30.08.2017
comment
volatile std::atomic<> отличается от обычного std::atomic<>. Однако оба они подразумевают возможную асинхронную модификацию @nwp. stackoverflow.com/a/2479474/224132 - person Peter Cordes; 31.08.2017
comment
@ MichaëlRoy: В MSVC volatile подразумевает некоторый порядок, поэтому он выходит за рамки того, что volatile означает в C ++ 11. Цитировать примеры из MSVC совершенно бессмысленно. Так мы говорим о значении чего-то вроде asm volatile() в GNU C. Оба эти расширения являются языковыми расширениями и ничего не говорят нам о том, что означает volatile в ISO C ++ 11. См. Также связь std :: memory_order с volatile в cppreference . - person Peter Cordes; 31.08.2017
comment
volatile atomic<int> y фактически запретил бы эту оптимизацию, потому что это подразумевает, что магазин может иметь побочный эффект. (В стандарте не упоминаются устройства ввода-вывода, но IIRC описывает volatile обращения как те, которые могут иметь побочные эффекты.) - person Peter Cordes; 31.08.2017
comment
@Peter То же самое относится к gcc и любым другим компиляторам. Гарантии, предлагаемые volatile, на самом деле являются подмножеством гарантий, предлагаемых std :: atomic. И именно по этой причине вы обнаружите, что все реализации std :: atomic, не только msvc, но и boost, gcc и clang также используют ключевое слово. std :: atomic просто не будет работать, если в какой-то момент удерживаемая переменная не будет повышена до изменчивой переменной. Ключевое слово указывает компилятору поддерживать порядок. Иначе был бы хаос. - person Michaël Roy; 31.08.2017
comment
Перекрытие есть, но volatile не является строгим подмножеством atomic. Заголовки компилятора имеют volatile atomic повсюду по той же причине, по которой другие заголовки имеют const повсюду: поэтому вы можете использовать их на volatile atomic типах, а также на обычных atomic типах. stackoverflow .com / questions / 2479067 /. - person Peter Cordes; 31.08.2017
comment
volatile не дает никаких гарантий для заказа, который другие потоки увидят в ваших магазинах. В компиляторах, отличных от MSVC, он имеет не более mo_relaxed. Компиляторы предоставляют возможность атомарного упорядочивания напрямую. Для gcc см. gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins. html. __atomic_load_n(ptr, __ATOMIC_ACQUIRE); работает без заголовков (godbolt.org/g/nrW8ak для вывода asm x86 и powerpc), это чистый встроенный компилятор, потому что в gcc встроены разные порядки. volatile не является обязательной частью <atomic>. - person Peter Cordes; 31.08.2017
comment
Кто сказал volatile atomic ?? Уж точно не я. Очевидно, вы не читали ни stl, ни код boost. - person Michaël Roy; 31.08.2017
comment
И __atomic_load_n зависит от компилятора, это также не библиотечная функция, а внутренняя функция, встроенная в компилятор. - person Michaël Roy; 31.08.2017
comment
И вы думаете, что заголовки VS2017 не специфичны для компилятора? / facepalm. Кроме того, функции, которые вы цитируете в своем ответе, используют volatile или const volatile в функциях точно так же, как я говорил: чтобы разрешить использование этих функций-членов для volatile atomic<T> объектов. например bool _Is_lock_free() const volatile. Если бы им было наплевать на volatile atomic, они бы вообще не использовали ключевое слово volatile. - person Peter Cordes; 31.08.2017
comment
Спустя 25 лет я уверен, что могу сказать разницу между ANSI-совместимым C ++ и встроенным компилятором. gcc.gnu.org/onlinedocs/gcc-4.1. 0 / gcc / Atomic-Builtins.html ... Я потерял дар речи. - person Michaël Roy; 01.09.2017
comment
@ MichaëlRoy Ссылка, которую вы предоставили, предназначена для расширений GCC для C в gcc 4.1.0 более 11 лет назад. В современном C ++ все изменилось. - person janm; 14.09.2017
comment
gcc по-прежнему поддерживает эту внутреннюю. - person Michaël Roy; 14.09.2017
comment
Спустя 25 лет вы не понимаете, что такое volatile в C / C ++. volatile не может помочь с реализацией атомики. - person curiousguy; 08.12.2018
comment
@nwp в частности volatile предполагает, что вы разговариваете не с памятью, а с устройствами, volatile ничего не предполагает о семантике доступа к объектам; единственное предположение состоит в том, что нет никакого предположения. Доступ к объекту имеет значение при работе с изменчивым объектом: любое изменчивое чтение должно смотреть на сохраненное значение, а изменчивая запись должна записывать новое значение. Это полезно, если вы намереваетесь использовать отладчик и изменить значение в этом объекте, когда программа приостановлена ​​на точке останова при непостоянном доступе: изменение значения в отладчике будет эквивалентно назначению C / C ++. - person curiousguy; 08.12.2018
comment
Так volatile действительно кое-что гарантирует при прохождении программы. (Это означает, что программа приостановлена.) Большинство применений volatile, вероятно, не связаны с внешними устройствами. volatile также можно использовать для реализации упорядочения потребляемой памяти понятным и реализуемым способом, в отличие от жалкого беспорядка, который представляет собой текущая спецификация потребления C ++. - person curiousguy; 08.12.2018
comment
@curiousguy: Нет. Atomic - это только программная функция. Это в основном не позволяет компилятору оптимизировать переменную в регистре. Это единственный эффект ключевого слова volatile. На сам процессор не влияет. - person Michaël Roy; 29.12.2018
comment
@ MichaëlRoy atomic - это только программная функция Вы имели в виду: volatile - это только программная функция? - person curiousguy; 25.10.2019
comment
@MaxLybbert поэтому компилятор должен фактически читать и записывать в переменную вместо кэширования предыдущих операций чтения / записи Что в тексте std предотвращает кеширование атомарных объектов? - person curiousguy; 25.10.2019
comment
@curiousguy Я имел в виду именно это. volatile - это программная функция, а atomic - аппаратная функция. Извините за путаницу. - person Michaël Roy; 25.10.2019