Чи пояснення розслабленого замовлення помилкове в cppreference?


13

У документації 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, отже, умовна частина останнього речення знімка ніколи не буде виконана.


Ваше питання конкретно щодо C ++ 11?
цікавогут

ні, це також стосується c ++ 14,17. Я знаю, що і компілятор, і процесор можуть змінити порядок на C за допомогою D.But, якщо відбудеться переупорядкування, C не може бути завершено до початку D. Отже, я думаю, що в реченні "A є послідовно-перед B у потоці 1, а C - перед D в межах потоку 2". Точніше сказати "У коді A розміщується до того, як B знаходиться в потоці 1, а C розміщується ДО D в межах потоку 2". Мета цього питання - підтвердити цю думку
abigaile

Нічого не визначено в терміні "переупорядкування".
цікавогут

Відповіді:


12

Я вважаю, що 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 повинно відбуватися перед D , але компілятор вважає, що не можна довести, чи сталося C або D далі і, завдяки правилу, ніби переставляє їх, все-таки правильно?
Fureeish

4
@Fureeish № C має відбуватися перед D, наскільки може розповісти нитка, на якій вони трапляються. Спостереження з іншого контексту може суперечити цьому погляду.
Дедуплікатор

Немає "як би правила"
цікавогут


1
@curiousguy Стандарт містить позначення одного із його положень "правилом як би" у виносці: "Це положення іноді називають введенням" як-ніби "правило. Виконання
Caleth

1

Іноді можна впорядкувати дію відносно двох інших послідовностей дій, не маючи на увазі жодного відносного впорядкування дій у цих послідовностях відносно один одного.

Припустимо, наприклад, що у трьох таких подій:

  • магазин 1 до p1
  • завантажте p2 у темп
  • зберігати від 2 до р3

і зчитування р2 незалежно впорядковується після запису р1 та перед записом р3, але немає особливого впорядкування, в якому беруть участь і р1, і р3. Залежно від того, що зроблено з p2, компілятору може бути недоцільно відкладати p1 минулого p3 і все-таки досягти необхідної семантики з p2. Припустимо, але компілятор знав, що вищевказаний код є частиною більшої послідовності:

  • зберігати від 1 до p2 [послідовно перед завантаженням p2]
  • [зробіть вище]
  • зберігати 3 в p1 [послідовно після іншого магазину до p1]

У цьому випадку він може визначити, що він може переупорядкувати магазин на p1 після вищевказаного коду та об'єднати його з наступним сховищем, таким чином, у результаті з'явиться код, який записує p3, не записуючи p1 спочатку:

  • встановити темп на 1
  • зберігати темп до p2
  • зберігати від 2 до р3
  • магазин 3 до р1

Хоча може здатися, що залежність даних призведе до того, що певні частини послідовних відносин поводяться транзитивно, компілятор може визначити ситуації, коли очевидних залежностей від даних не існує, і, таким чином, не матиме перехідних наслідків, яких можна було б очікувати.


1

Якщо є два твердження, компілятор генерує код у послідовному порядку, тому код для першого буде розміщений перед другим. Але внутрішні технології cpus мають трубопроводи і здатні паралельно виконувати операції збирання. Заява C - інструкція з навантаження. Під час отримання пам’яті конвеєр обробляє наступні кілька інструкцій і, враховуючи, що вони не залежать від інструкції щодо завантаження, вони можуть закінчитися виконанням до завершення роботи C (наприклад, дані для D були в кеші, C - в основній пам'яті).

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


-7

Що б ви думали, однаково справедливо. Стандарт не говорить про те, що виконується послідовно, що ні, і як це можна змішувати .

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.