Является ли объяснение упрощенного порядка ошибочным в 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

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

Все, что вы думаете, одинаково верно. В стандарте не сказано, что выполняется последовательно, а что нет и как это можно перепутать.

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

РЕДАКТИРОВАТЬ: Могут ли отрицательные голоса оправдать свою отрицательную реакцию?

Другие вопросы по тегам