Ответ 1
Да, потому что мы не можем наблюдать разницу!
Реализация позволяет превратить ваш фрагмент в следующую (псевдо-реализацию).
int __loaded_foo = foo;
int x = __loaded_foo;
int y = __loaded_foo;
Причина в том, что вы не можете наблюдать разницу между приведенной выше и двумя отдельными нагрузками foo, учитывая гарантии последовательной согласованности.
Примечание. Это не просто компилятор, который может сделать такую оптимизацию, процессор может просто рассуждать о том, что вы не можете наблюдать разницу и загружать значение
foo
один раз — даже если компилятор мог бы попросить его сделать это дважды.
Объяснение
Учитывая поток, который продолжает обновлять foo инкрементным образом, гарантируется, что y
будет иметь одно и то же значение или более позднее письменное значение по сравнению с содержимым x
.
// thread 1 - The Writer
while (true) {
foo += 1;
}
// thread 2 - The Reader
while (true) {
int x = foo;
int y = foo;
assert (y >= x); // will never fire, unless UB (foo has reached max value)
}
Представьте, что поток писем по какой-то причине приостанавливает выполнение на каждой итерации (из-за контекстного переключателя или другой причины, связанной с реализацией); вы не можете доказать, что это то, что приводит к тому, что оба x
и y
имеют одинаковое значение, или если это связано с "оптимизацией слияния".
Другими словами, мы имеем потенциальные исходы, учитывая код в этом разделе:
- Никакое новое значение не записывается в foo между двумя чтениями (
x == y
). - Новое значение записывается в foo между двумя чтениями (
x < y
).
Так как любое из двух может произойти, реализация может сузить область действия, чтобы просто выполнить один из них; мы никоим образом не можем наблюдать разницу.
Что говорит стандарт?
Реализация может делать любые изменения, которые она хочет, пока мы не можем наблюдать никакой разницы между поведением, которое мы выразили, и поведением во время выполнения.
Это описано в [intro.execution]p1
:
Семантические описания в этом Международном стандарте определяют параметризованной недетерминированной абстрактной машиной. Этот международный Стандарт не требует от структуры соответствия Реализации. В частности, им не нужно копировать или эмулировать структура абстрактной машины. Скорее, соответствующие реализации требуется для подражания (только) наблюдаемого поведения абстрактного машина, как описано ниже.
Другой раздел, который делает его еще более понятным [intro.execution]p5
:
Соответствующая реализация, выполняющая хорошо сформированную программу, должна производят такое же наблюдаемое поведение, как одно из возможных исполненийсоответствующего экземпляра абстрактной машины с тем же программы и того же ввода.
Дополнительная литература:
Как насчет опроса в цикле?
// initial state
std::atomic<int> foo = 0;
// thread 1
while (true) {
if (foo)
break;
}
// thread 2
foo = 1
Вопрос. Учитывая рассуждения в предыдущих разделах, может ли реализация просто читать
foo
один раз в потоке 1, а затем никогда не выходить из цикла, даже если поток 2 записывается вfoo
Ответ; Нет.
В последовательной согласованной среде мы гарантируем, что запись в foo в потоке 2 станет видимой в потоке 1; это означает, что когда эта запись произошла, поток 1 должен наблюдать это изменение состояния.
Примечание. Реализация может превратить два чтения в один, потому что мы не можем наблюдать разницу (один забор так же эффективен, как и два), но он не может полностью игнорировать прочитанное, которое существует сам по себе.
Примечание. Содержимое этого раздела гарантируется [atomics.order]p3-4
.
Что делать, если я действительно хочу предотвратить эту форму "оптимизации"?
Если вы хотите, чтобы реализация фактически прочитала значение некоторой переменной в каждой точке, где вы ее написали, вы должны изучить использование volatile
(обратите внимание, что это никоим образом не повышает безопасность потоков).