Является ли объяснение расслабленного порядка ошибочным в сравнении? - PullRequest
13 голосов
/ 11 января 2020

В документации std::memory_order на cppreference.com есть пример расслабленного заказа:

Расслабленный заказ

Атоми c операции отмеченные 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 ]

13 голосов
/ 11 января 2020

Я считаю, что cppreference - это правильно. Я думаю, что это сводится к правилу «как будто» [intro.execution] / 1 . Компиляторы обязаны воспроизводить только наблюдаемое поведение программы, описанное вашим кодом. Отношение sequenced-before устанавливается только между оценками с точки зрения потока, в котором эти оценки выполняются [intro.execution] / 15 . Это означает, что когда две оценки, последовательно расположенные одна за другой, появляются где-то в каком-то потоке, код, фактически выполняющийся в этом потоке, должен вести себя , как если бы независимо от того, что первая оценка действительно влияла на то, что делает вторая оценка. Например,

int x = 0;
x = 42;
std::cout << x;

должен print 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 действительно может произойти ...

1 голос
/ 11 января 2020

Если есть два оператора, компилятор будет генерировать код в последовательном порядке, поэтому код для первого будет размещен перед вторым. Но внутри процессора есть конвейеры, и они могут выполнять сборочные операции параллельно. Оператор C является инструкцией загрузки. Во время извлечения памяти конвейер будет обрабатывать следующие несколько инструкций, и, учитывая, что они не зависят от инструкции загрузки, они могут закончиться выполнением до завершения C (например, данные для D находились в кеше, C в основном память).

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

1 голос
/ 11 января 2020

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

Предположим, например, что одно имеет следующие три события:

  • сохранить 1 в p1
  • загрузить p2 в temp
  • сохранить 2 в p3

и чтение p2 независимо упорядочивается после записи p1 и перед записью p3, но нет особого порядка, в котором участвуют как p1, так и p3. В зависимости от того, что сделано с p2, компилятору может быть нецелесообразно отложить p1 за p3 и все же достичь требуемой семантики с p2. Предположим, однако, что компилятор знал, что приведенный выше код был частью более крупной последовательности:

  • сохранить от 1 до p2 [упорядочено до загрузки p2]
  • [сделать выше]
  • сохранить 3 в p1 [упорядоченный после другого хранилища в p1]

В этом случае он может определить, что он может изменить порядок хранилища в p1 после приведенного выше кода и консолидировать его со следующим хранилищем, в результате чего получается код, который записывает p3 без записи p1:

  • установить temp на 1
  • сохранить temp на p2
  • store 2 на p3
  • store 3 to p1

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

0 голосов
/ 12 января 2020

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

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

...