Является ли объяснение упрощенного порядка ошибочным в cppreference?
В документацииstd::memory_order
на cppreference.com есть пример упрощенного заказа:
Удобный заказ
Атомарные операции помечены
memory_order_relaxed
не являются операциями синхронизации; они не устанавливают порядок между одновременными обращениями к памяти. Они гарантируют только атомарность и согласованность порядка модификации.Например, если x и y изначально равны нулю,
// Thread 1: r1 = y.load(std::memory_order_relaxed); // A x.store(r1, std::memory_order_relaxed); // B // Thread 2: r2 = x.load(std::memory_order_relaxed); // C y.store(42, std::memory_order_relaxed); // D
разрешено производить r1 == r2 == 42, потому что, хотя A упорядочен до B в потоке 1 и C упорядочен до D в потоке 2, ничто не мешает D появиться перед A в порядке модификации y, а B от появляется перед C в порядке модификации x. Побочный эффект D на y может быть виден для нагрузки A в потоке 1, в то время как побочный эффект B на x может быть виден для нагрузки C в потоке 2. В частности, это может произойти, если D завершается до C в поток 2, либо из-за переупорядочения компилятора, либо во время выполнения.
он говорит: "C упорядочен перед D в потоке 2".
Согласно определению упорядоченного до, которое можно найти в Порядке оценки, если A упорядочен до B, то оценка A будет завершена до начала оценки B. Поскольку C упорядочивается перед D в потоке 2, C должен быть завершен до начала D, поэтому часть условия последнего предложения моментального снимка никогда не будет удовлетворена.
4 ответа
Я считаю, что cppreference правильно. Я думаю, что это сводится к правилу "как если бы" [intro.execution] / 1. Компиляторы обязаны воспроизводить только наблюдаемое поведение программы, описанное вашим кодом. Секвенировал-прежде, чем отношение только устанавливается между оценками с точки зрения потока, в котором эти оценки выполняются [intro.execution] / 15. Это означает, что когда две оценки, упорядоченные одна за другой, появляются где-то в каком-то потоке, код, фактически выполняющийся в этом потоке, должен вести себя так, как будто все, что делает первая оценка, действительно влияет на то, что делает вторая оценка. Например
int x = 0;
x = 42;
std::cout << x;
должен напечатать 42. Однако компилятору на самом деле не нужно сохранять значение 42 в объекте.x
перед чтением значения из этого объекта, чтобы распечатать его. С таким же успехом можно помнить, что последнее значение, сохраняемое вx
было 42, а затем просто распечатайте значение 42 непосредственно перед фактическим сохранением значения 42 в x
. Фактически, еслиx
является локальной переменной, она может также просто отслеживать, какое значение этой переменной было присвоено последним в любой момент, и никогда даже не создавать объект или фактически сохранять значение 42. У потока нет способа определить разницу. Поведение всегда будет таким, как если бы существовала переменная и как если бы значение 42 было фактически сохранено в объекте.x
перед загрузкой из этого объекта. Но это не означает, что сгенерированный машинный код должен фактически хранить и загружать что-либо где-нибудь и когда-либо. Все, что требуется, - это чтобы наблюдаемое поведение сгенерированного машинного кода было неотличимо от того, каким было бы поведение, если бы все эти вещи действительно произошли.
Если мы посмотрим на
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D
тогда да, C упорядочивается до D. Но если смотреть из этого потока изолированно, ничто из того, что делает C, не влияет на результат D. И ничто из того, что делает D, не изменит результат C.Единственный способ, которым одно может повлиять на другое, - как косвенное следствие того, что что-то происходит в другом потоке. Однако, указавstd::memory_order_relaxed
, вы явно заявили, что порядок, в котором загрузка и сохранение наблюдаются другим потоком, не имеет значения. Поскольку никакой другой поток не может наблюдать загрузку и сохранение в каком-либо конкретном порядке, другой поток не может сделать ничего, чтобы заставить C и D воздействовать друг на друга согласованным образом. Таким образом, порядок, в котором фактически выполняются загрузка и сохранение, не имеет значения. Таким образом, компилятор может изменить их порядок. И, как упоминалось в пояснении под этим примером, если сохранение из D выполняется до загрузки из C, тогда действительно может произойти r1 == r2 == 42...
Если есть два оператора, компилятор сгенерирует код в последовательном порядке, поэтому код для первого будет помещен перед вторым. Но внутри ЦП есть конвейеры, и они могут выполнять операции сборки параллельно. Оператор C - это инструкция загрузки. Пока выполняется выборка из памяти, конвейер будет обрабатывать следующие несколько инструкций, и, учитывая, что они не зависят от инструкции загрузки, они могут быть выполнены до завершения C (например, данные для D были в кэше, C в основной памяти).
Если пользователю действительно нужно, чтобы два оператора выполнялись последовательно, можно использовать более строгие операции упорядочивания памяти. В общем, пользователям все равно, если программа логически верна.
Иногда действие можно упорядочить относительно двух других последовательностей действий, не подразумевая какой-либо относительный порядок действий в этих последовательностях относительно друг друга.
Предположим, например, что у вас есть следующие три события:
- магазин 1 в p1
- загрузить p2 в темп
- магазин 2 в p3
и чтение p2 независимо упорядочивается после записи p1 и перед записью p3, но нет особого порядка, в котором участвуют как p1, так и p3. В зависимости от того, что делается с p2, для компилятора может быть непрактично откладывать p1 за p3 и по-прежнему достигать требуемой семантики с p2. Однако предположим, что компилятор знал, что приведенный выше код является частью более крупной последовательности:
- сохранить 1 в p2 [последовательность перед загрузкой p2]
- [сделайте то же самое]
- сохранить 3 в p1 [последовательно после другого сохранения в p1]
В этом случае он может определить, что может переупорядочить хранилище на p1 после приведенного выше кода и объединить его со следующим хранилищем, в результате чего получится код, который записывает p3 без предварительной записи p1:
- установить температуру на 1
- сохранить температуру на p2
- магазин 2 в p3
- магазин 3 в p1
Хотя может показаться, что зависимости данных могут привести к транзитивному поведению определенных частей отношений последовательности, компилятор может идентифицировать ситуации, в которых очевидных зависимостей данных не существует, и, следовательно, не будет иметь переходных эффектов, которых можно было бы ожидать.
Все, что вы думаете, одинаково верно. В стандарте не сказано, что выполняется последовательно, а что нет и как это можно перепутать.
Вам и каждому программисту нужно создать согласованную семантику поверх этого беспорядка, работа, достойная нескольких кандидатов наук.
РЕДАКТИРОВАТЬ: Могут ли отрицательные голоса оправдать свою отрицательную реакцию?