Чи мінливе ключове слово C ++ вводить паркан пам'яті?


85

Я розумію, що volatileповідомляє компілятору про те, що значення може бути змінено, але для того, щоб виконати цю функціональність, чи потрібно компілятору вводити огорожу пам'яті, щоб він працював?

Наскільки я розумію, послідовність операцій з нестабільними об’єктами не може бути впорядкована і повинна бути збережена. Це, мабуть, означає, що деякі огорожі пам'яті необхідні, і що насправді цього немає. Чи правильно я це кажу?


З цим пов’язаним питанням відбувається цікава дискусія

Джонатан Уейклі пише :

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

На що Девід Шварц відповідає у коментарях :

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

... Стандарт C ++ не робить жодної різниці щодо того, що робить переупорядкування. І ви не можете сперечатися з тим, що центральний процесор може переупорядковувати їх без спостережуваного ефекту, тому це нормально - стандарт С ++ визначає їх порядок як спостережуваний. Компілятор відповідає стандарту C ++ на платформі, якщо він генерує код, який змушує платформу робити те, що вимагає стандарт. Якщо стандарт вимагає, щоб доступ до летких речовин не переупорядковувався, то платформа, яка переупорядковує їх, не відповідає вимогам. ...

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

Що дає два запитання: чи є одне з них "правильним"? Що насправді роблять фактичні реалізації?


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


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

Наскільки я знаю, волатильність використовується для змінних, які можуть бути змінені апаратним забезпеченням (часто використовуються з мікроконтролерами). Це просто означає, що читання змінної не можна робити в іншому порядку і не можна оптимізувати. Це C, хоча, але має бути однаковим у ++.
Щогла

1
@Mast Я ще не бачив компілятора, який не дозволяє volatileоптимізувати зчитування змінних кешами центрального процесора. Або всі ці компілятори не відповідають нормам, або стандарт не означає, що ви думаєте, що це означає. (Стандарт не робить різниці між тим, що робить компілятор, і тим, що робить компілятор процесором. Завдання компілятора - видавати код, який при запуску відповідає стандарту.)
Девід Шварц,

Відповіді:


58

Замість того, щоб пояснювати, що саме volatileробить, дозвольте мені пояснити, коли вам слід використовувати volatile.

  • Коли знаходиться всередині обробника сигналу. Оскільки запис у volatileзмінну - це майже єдине, що стандарт дозволяє робити з обробника сигналу. Починаючи з C ++ 11, ви можете використовувати std::atomicдля цієї мети, але лише за умови, що атомар не має блокування.
  • При роботі з компанією setjmp Intel .
  • Маючи справу безпосередньо з апаратним забезпеченням, і ви хочете переконатись, що компілятор не оптимізує читання чи запис.

Наприклад:

volatile int *foo = some_memory_mapped_device;
while (*foo)
    ; // wait until *foo turns false

Без volatileспецифікатора компілятору дозволено повністю оптимізувати цикл. Специфікатор volatileповідомляє компілятору, що він не може вважати, що 2 наступних читання повертають одне і те ж значення.

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

У всіх інших випадках використання volatileслід вважати не портативним і більше не проходити перевірку коду, крім випадків, коли йдеться про компілятори та розширення компілятора до C ++ 11 (наприклад, /volatile:msперемикач msvc , який за замовчуванням увімкнено під X86 / I64).


5
Це суворіше, ніж "не можна вважати, що 2 наступних читання повертають одне і те ж значення". Навіть якщо ви читаєте лише один раз та / або викидаєте значення (-а), читання має бути виконане.
philipxy

1
Використання в обробниках сигналів і setjmpє двома гарантіями, які робить стандарт. З іншого боку, наміром , принаймні на початку, була підтримка відображеного в пам'яті вводу-виводу. Для деяких процесорів може знадобитися огорожа або мембрана.
James Kanze,

@philipxy Окрім того, що ніхто не знає, що означає "прочитане". Наприклад, ніхто не вважає, що потрібно здійснити фактичне читання з пам'яті - жоден відомий компілятор не намагається обійти кеш-пам’яті процесора при volatileдоступі.
Девід Шварц

@JamesKanze: Не так. Стандарт обробників сигналів Re стверджує, що під час обробки сигналу визначені значення мають лише мінливі std :: sig_atomic_t & free-lock атомні об'єкти. Але там також сказано, що доступ до летких об'єктів є спостережуваними побічними ефектами.
philipxy

1
@DavidSchwartz Деякі пари компілятора та архітектури відображають стандартну послідовність звернень до фактичних ефектів, а робочі програми мають доступ до летких елементів, щоб отримати ці ефекти. Той факт, що деякі такі пари не мають зіставлення чи тривіального непотрібного відображення, має значення для якості реалізацій, але не для суті справи.
philipxy

25

Чи мінливе ключове слово C ++ вводить паркан пам'яті?

Компілятор C ++, який відповідає специфікації, не вимагає введення забору пам'яті. Ваш конкретний компілятор може; направити своє запитання авторам вашого компілятора.

Функція "volatile" в C ++ не має нічого спільного з потоками. Пам'ятайте, що мета "нестабільного" полягає в тому, щоб відключити оптимізацію компілятора, щоб зчитування з реєстру, який змінюється внаслідок екзогенних умов, не було оптимізовано. Чи є адреса пам'яті, на яку записаний інший потік на іншому ЦП, регістром, який змінюється внаслідок екзогенних умов? Ні. Знову ж таки, якщо деякі автори компіляторів вирішили розглядати адреси пам'яті, на які записуються різні потоки на різних центральних процесорах, ніби це реєстри, що змінюються внаслідок екзогенних умов, це їх справа; вони не зобов’язані цього робити. Вони також не потрібні - навіть якщо це вводить огорожу пам'яті - щоб, наприклад, переконатися, що кожен потік бачить послідовний упорядкування мінливих читань і записів.

Насправді, летючі властивості майже не потрібні для створення потоків у C / C ++. Найкраща практика - уникати цього.

Більше того: паркани пам'яті є деталлю реалізації конкретних архітектур процесорів. У C #, де летючий явно буде призначений для багатопотокового, специфікація не говорить , що половина огорожі буде введена, так як програма може бути запущена на архітектурі , яка не має огорож в першу чергу. Швидше, знову ж таки, специфікація дає певні (надзвичайно слабкі) гарантії щодо того, яких оптимізацій ухиляються компілятор, час виконання та процесор, щоб встановити певні (надзвичайно слабкі) обмеження щодо того, як будуть впорядковані деякі побічні ефекти. На практиці ці оптимізації усуваються використанням напівзаборів, але це деталь реалізації, яка може змінитися в майбутньому.

Той факт, що ви дбаєте про семантику volatile будь-якою мовою, оскільки вони стосуються багатопоточності, вказує на те, що ви думаєте про спільне використання пам'яті між потоками. Подумайте, просто не робити цього. Це робить вашу програму набагато складнішою для розуміння та набагато більше шансів містити тонкі помилки, які неможливо відтворити.


19
"volatile в C / C ++ майже марний." Зовсім не! У вас дуже орієнтований на користувальницький робочий стіл погляд на світ ... але більшість кодів C та C ++ працює на вбудованих системах, де мінливість дуже необхідна для відображеного в пам'яті вводу-виводу.
Ben Voigt,

12
І причина того, що нестабільний доступ зберігається, полягає не просто в тому, що екзогенні умови можуть змінити місце пам’яті. Сам доступ може спровокувати подальші дії. Наприклад, дуже часто читання просуває FIFO або очищає прапор переривання.
Ben Voigt,

3
@BenVoigt: Я мав на увазі марність для ефективної боротьби з неприємностями, що нарізують нитки.
Ерік Ліпперт,

4
@DavidSchwartz Стандарт, очевидно, не може гарантувати, як працює вкладений вхідний пам'ять. Але volatileвведений в пам'ять IO - ось чому був введений у стандарт C. Тим не менше, оскільки стандарт не може вказувати такі речі, як те, що насправді відбувається під "доступом", він говорить, що "Те, що становить доступ до об'єкта, який має нестабільний тип, визначається реалізацією". Занадто багато реалізацій сьогодні не дають корисного визначення доступу, який IMHO порушує дух стандарту, навіть якщо він відповідає букві.
James Kanze,

8
Це редагування є певним покращенням, але ваше пояснення все ще надто зосереджено на тому, що "пам'ять може бути змінена екзогенно". volatileсемантика сильніша за це, компілятор повинен генерувати кожен запитуваний доступ (1.9 / 8, 1.9 / 12), а не просто гарантувати, що в кінцевому підсумку будуть виявлені екзогенні зміни (1.10 / 27). У світі відображеного вводу-виводу пам'яті зчитування пам'яті може мати довільно пов’язану логіку, подібно до властивості отримання. Ви не зможете оптимізувати дзвінки до майстрів, що отримують, відповідно до правил, які ви вказали volatile, а також Стандарт не дозволяє цього.
Бен Войгт

13

Девід ігнорує той факт, що стандарт С ++ визначає поведінку декількох потоків, які взаємодіють лише у конкретних ситуаціях, а все інше призводить до невизначеної поведінки. Умова раси, що включає принаймні одну запис, невизначена, якщо ви не використовуєте атомні змінні.

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


5
Гарно пояснив, дякую. Стандарт визначає послідовність доступу до летких речовин, яку можна спостерігати , якщо програма не має невизначеної поведінки .
Джонатан Уейклі

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

Чому ви думаєте, що я це пропускаю? Як ви думаєте, яка частина мого аргументу призводить до недійсності? Я на 100% погоджуюсь з тим, що компілятор цілком в своєму праві відмовитися від будь-якої синхронізації.
Девід Шварц,

2
Це просто неправильно, або, принаймні, воно ігнорує суттєве. volatileне має нічого спільного з нитками; первісною метою було підтримка відображеного вводу-виводу пам'яті. І принаймні на деяких процесорах для підтримки введеного в пам'ять вводу-виводу потрібно огорожа. (Укладачі цього не роблять, але це вже інше питання.)
Джеймс Канце,

@JamesKanze volatileмає багато спільного з потоками: volatileмає справу з пам'яттю, до якої можна отримати доступ, а компілятор не знає, що до неї можна отримати доступ, і яка охоплює багато випадків використання спільних даних між потоками на конкретному процесорі.
curiousguy

12

Перш за все, стандарти C ++ не гарантують бар'єри пам'яті, необхідні для належного впорядкування неатомних читання / запису. Летючі змінні рекомендується для використання з MMIO, обробки сигналів і т.д. У більшості реалізацій летючими не є корисним для многопоточности , і це не рекомендується.

Щодо реалізації нестабільних доступів, це вибір компілятора.

Ця стаття , що описує поведінку gcc, показує, що ви не можете використовувати леткий об'єкт як бар'єр пам'яті, щоб упорядковувати послідовність записів у енергозалежну пам'ять.

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

Компілятор Microsoft VS2013 має іншу поведінку. У цій документації пояснюється, як енергозалежність застосовує семантику Release / Acquire та дозволяє використовувати енергонезалежні об'єкти в блокуваннях / версіях у багатопотокових програмах.

Іншим аспектом, який потрібно враховувати, є те, що один і той же компілятор може мати різну поведінку wrt. до мінливості залежно від цільової апаратної архітектури . У цій публікації щодо компілятора MSVS 2013 чітко зазначено особливості компіляції з мінливими для ARM платформ.

Тож моя відповідь на:

Чи мінливе ключове слово C ++ вводить паркан пам'яті?

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


2
Це не заважає оптимізації, воно просто заважає компілятору змінювати навантаження та зберігати за певні обмеження.
Дітріх Епп,

Не зрозуміло, що ви говорите. Ви хочете сказати, що це трапляється у деяких невизначених компіляторах, що volatileзаважає компілятору переупорядковувати навантаження / сховища? Або ви кажете, що стандарт С ++ вимагає, щоб він це робив? А якщо останнє, чи можете ви відповісти на мій аргумент протилежним, наведеним у вихідному питанні?
Девід Шварц,

@DavidSchwartz Стандарт запобігає переупорядкуванню (з будь-якого джерела) доступу через значення volatilelvalue. Оскільки це залишає визначення поняття "доступ" до реалізації, однак, це не надто купує нас, якщо впровадження байдуже.
James Kanze,

Думаю, деякі версії компіляторів MSC впроваджували семантику забору volatile, але в створеному коді від компілятора у Visual Studios 2012 немає забору
Джеймс Канзе,

@JamesKanze Що в основному означає, що єдиною портативною поведінкою користувача volatileє та, яка спеціально перерахована стандартом. ( setjmp, сигнали тощо)
Девід Шварц

7

Наскільки мені відомо, компілятор вставляє лише огорожу пам'яті в архітектуру Itanium.

volatileКлючове слово дійсно найкраще використовувати для асинхронних змін, наприклад, обробників сигналів і відображених в пам'яті регістрів; зазвичай це неправильний інструмент для багатопотокового програмування.


1
Типу. 'компілятор' (msvc) вставляє огорожу пам'яті, коли націлена архітектура, відмінна від ARM, і використовується перемикач / volatile: ms (за замовчуванням). Див. Msdn.microsoft.com/en-us/library/12a04hfd.aspx . Інші компілятори, на мою думку, не вставляють огорожі з мінливих змінних. Слід уникати використання енергонезалежних, якщо це стосується безпосередньо апаратного забезпечення, обробників сигналів або компіляторів, що не відповідають стандарту C ++ 11.
Стефан

@Stefan No. volatileнадзвичайно корисний для багатьох видів використання, які ніколи не мають справу з апаратним забезпеченням. Всякий раз, коли ви хочете, щоб реалізація генерувала код процесора, який точно відповідає коду C / C ++, використовуйте volatile.
curiousguy

7

Це залежить від того, який компілятор є "компілятором". Visual C ++ це робить з 2005 року. Але стандарт цього не вимагає, тому деякі інші компілятори цього не роблять.


VC ++ 2012 не здається , щоб вставити паркан: int volatile i; int main() { return i; }генерує головний рівно дві інструкції: mov eax, i; ret 0;.
James Kanze,

@JamesKanze: Яка саме версія? І чи використовуєте ви якісь варіанти компіляції, що не за замовчуванням? Я покладаюсь на документацію (перша версія, що зазнала впливу) та (остання версія) , де однозначно згадується семантика придбання та випуску.
Ben Voigt,

cl /helpговорить версія 18.00.21005.1. Каталог, в якому він знаходиться, є C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC. У заголовку командного вікна написано VS 2013. Тож щодо версії ... Єдиними опціями, якими я користувався, були /c /O2 /Fa. (Без цього /O2він також встановлює локальну рамку стека. Але досі немає інструкцій щодо забору.)
Джеймс Канзе,

@JamesKanze: Мене більше цікавила архітектура, наприклад, "Microsoft (R) C / C ++ Optimizing Compiler Version 18.00.30723 for x64" Можливо, немає жодної огорожі, оскільки x86 та x64 мають досить потужні гарантії когерентності кешу в своїй моделі пам'яті. ?
Бен Войгт

Можливо. Я насправді не знаю. Той факт, що я зробив це в mainтому, щоб компілятор міг бачити всю програму і знати, що не було інших потоків або, принаймні, жодних інших звернень до змінної до моєї (тому не могло бути проблем з кешем), міг вплинути на це також, але якось, я сумніваюся.
James Kanze,

5

Це значною мірою з пам'яті та засноване на попередній версії C ++ 11, без потоків. Але, беручи участь в обговоренні потоків у комітеті, я можу сказати, що комітет ніколи не мав наміру, який volatileміг би бути використаний для синхронізації між потоками. Корпорація Майкрософт запропонувала це рішення, але пропозиція не допомогла.

Ключовою специфікацією volatileє те, що доступ до летючої речовини являє собою "спостережувану поведінку", як і IO. Таким же чином компілятор не може змінити порядок або видалити конкретний ввід-вивід, він не може змінити порядок або видалити доступ до летючого об'єкта (або, більш правильно, доступ через вираз lvalue з нестабільним кваліфікованим типом). По суті, початковий намір volatile полягав у підтримці відображеного пам’яті IO. Однак "проблема" цього полягає в тому, що саме реалізація визначає, що становить "нестабільний доступ". І багато компіляторів реалізують це так, ніби визначення було "виконана інструкція, яка читає або записує в пам'ять". Що є легальним, хоч і марним визначенням, якщо реалізація це визначає. (Я ще не знайшов фактичної специфікації для будь-якого компілятора.

Можна стверджувати (і це аргумент, який я приймаю), це порушує намір стандарту, оскільки, якщо апаратне забезпечення не розпізнає адреси як відображене в пам'ять введення-виведення та не забороняє будь-яке впорядкування тощо, ви навіть не можете використовувати енергонезалежні для відображеного в пам'яті вводу-виводу, принаймні на архітектурах Sparc або Intel. Тим не менше, жоден із переглядачів, які я розглядав (Sun CC, g ++ та MSC), не видає жодних інструкцій щодо забору чи мембрани. (Приблизно в той час, коли Microsoft запропонувала розширити правила volatile, я думаю, що деякі їх компілятори реалізували свою пропозицію та видали інструкції щодо забору для нестабільних доступів. Я не перевірив, що роблять останні компілятори, але це не здивувало б мене, якби це залежало на якомусь варіанті компілятора. Проте версія, яку я перевірив - я думаю, що це був VS6.0 - однак не видавала огорож.)


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

@DavidSchwartz, бо саме так говорить стандарт. Звичайно, з практичної точки зору, те, що роблять перевірені мною компілятори, абсолютно марно, але стандартних слів ласки цього достатньо, щоб вони все ще могли заявляти про відповідність (або могли б, якщо вони насправді це документували).
James Kanze,

1
@DavidSchwartz: Для ексклюзивного (або зміненого) відображеного в пам'яті вводу-виводу для периферійних пристроїв volatileсемантика є цілком адекватною. Як правило, такі периферійні пристрої повідомляють про свої області пам'яті, що не кешуються, що допомагає впорядкувати на апаратному рівні.
Ben Voigt,

@BenVoigt Я якось дивувався цьому: ідея про те, що процесор якось "знає", що адреса, з якою він має справу, - це відображена пам'ять IO. Наскільки я знаю, Sparcs не має ніякої підтримки для цього, тому це все одно зробить Sun CC та g ++ на Sparc непридатними для вкладеного в пам'ять IO. (Коли я розбирався в цьому, мене в основному цікавив Sparc.)
Джеймс Канце,

@JamesKanze: З того незначного пошуку, який я зробив, схоже, що Sparc виділив діапазони адрес для "альтернативних поглядів" пам'яті, які не кешуються. Поки ваші нестабільні точки доступу потрапляють до ASI_REAL_IOчастини адресного простору, я думаю, ви повинні бути в порядку. (Altera NIOS використовує подібну техніку, з високими бітами адреси, що контролює обхід MMU; я впевнений, що є й інші)
Бен Войгт,

5

Не обов’язково. Volatile не є примітивом синхронізації. Це просто відключає оптимізацію, тобто ви отримуєте передбачувану послідовність читання та запису в потоці в тому самому порядку, як це передбачено абстрактною машиною. Але читання та запис у різних потоках не має порядку в першу чергу, немає сенсу говорити про збереження чи недотримання їхнього порядку. Порядок між тезами можна встановити за допомогою примітивів синхронізації, ви отримуєте UB без них.

Трохи пояснень щодо бар’єрів пам’яті. Типовий процесор має кілька рівнів доступу до пам'яті. Існує конвеєр пам'яті, кілька рівнів кеш-пам’яті, потім оперативна пам’ять тощо.

Інструкції Membar промивають трубопровід. Вони не змінюють порядок виконання читання та запису, це просто змушує виконувати непогашені в певний момент. Це корисно для багатопотокових програм, але не надто інакше.

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


1
Отже, ви говорите, що стандарт C ++ каже, що volatileпросто відключає оптимізацію компілятора? Це не має жодного сенсу. Будь-яка оптимізація, яку може зробити компілятор, може, принаймні в принципі, однаково добре виконуватися процесором. Отже, якби стандарт сказав, що він просто відключив оптимізацію компілятора, це означало б, що це не забезпечить жодної поведінки, на яку взагалі можна покластись у портативному коді. Але це, очевидно, не відповідає дійсності, оскільки портативний код може покладатися на свою поведінку щодо setjmpсигналів та сигналів.
Девід Шварц

1
@DavidSchwartz Ні, стандарт не говорить про таке. Вимкнення оптимізації - це саме те, що зазвичай роблять для впровадження стандарту. Стандарт вимагає, щоб спостережувана поведінка відбувалася в тому самому порядку, як вимагає абстрактна машина. Коли абстрактна машина не вимагає жодного замовлення, реалізація може вільно використовувати будь-яке замовлення або взагалі жодне. Доступ до змінних змінних у різних потоках не впорядковується, якщо не застосовується додаткова синхронізація.
п. 'займенники' m.

1
@DavidSchwartz Прошу вибачення за неточні формулювання. Стандарт не вимагає відключення оптимізації. Він взагалі не має поняття оптимізації. Швидше, він визначає поведінку, яка на практиці вимагає від компіляторів відключення певних оптимізацій таким чином, щоб спостережувана послідовність читання та запису відповідала стандарту.
п. 'займенники' m.

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

1
Ні, реалізація повинна визначити, що являє собою єдиний доступ. Послідовність таких звернень прописується абстрактною машиною. Реалізація повинна зберігати порядок. Стандарт прямо говорить, що "нестабільність - це натяк на реалізацію, щоб уникнути агресивної оптимізації за участю об'єкта", хоча і в ненормативній частині, але намір зрозумілий.
п. 'займенники' m.

4

Компілятору потрібно запровадити огорожу пам'яті навколо volatileзвернень тоді і лише тоді, коли це необхідно для використання для volatileзазначених у стандартній роботі ( setjmp, обробників сигналів тощо) на цій конкретній платформі.

Зверніть увагу, що деякі компілятори виходять далеко за межі того, що вимагає стандарт C ++, щоб зробити volatileбільш потужними або корисними на цих платформах. Портативний код не повинен покладатися на volatileбудь-які дії, що перевищують те, що зазначено в стандарті С ++.


2

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

Я роблю це для оперативної пам’яті, а також для введеного вводу в пам’ять.

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

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


0

Я думаю, плутанина навколо нестабільності та переупорядкування інструкцій випливає з двох понять про переупорядкування процесорів:

  1. Виконання поза замовленням.
  2. Послідовність читання / запису пам'яті, як це бачать інші процесори (переупорядковуючи в тому сенсі, що кожен процесор може бачити іншу послідовність).

Volatile впливає на те, як компілятор генерує код, припускаючи однопотокове виконання (сюди входять переривання). Це не означає нічого про інструкції щодо бар'єру пам'яті, але навпаки, виключає компілятор для виконання певних видів оптимізації, пов'язаних з доступом до пам'яті.
Типовим прикладом є повторне отримання значення з пам'яті, замість використання кешованого в реєстрі.

Виконання поза замовленням

Процесори можуть виконувати інструкції не в порядку / спекулятивно за умови, що кінцевий результат міг статися в оригінальному коді. Процесори можуть виконувати перетворення, заборонені компіляторами, оскільки компілятори можуть виконувати лише перетворення, які є правильними за будь-яких обставин. На відміну від цього, центральні процесори можуть перевірити обґрунтованість цих оптимізацій і відмовитись від них, якщо вони виявляються неправильними.

Послідовність читання / запису пам'яті, як це бачать інші процесори

Кінцевий результат послідовності інструкцій, ефективний порядок, повинен узгоджуватися із семантикою коду, сформованого компілятором. Однак фактичний порядок виконання, обраний ЦП, може бути різним. Ефективний порядок, як це видно на інших процесорах (кожен процесор може мати різний вигляд), може бути обмежений бар'єрами пам'яті.
Я не впевнений, наскільки ефективне та фактичне замовлення можуть відрізнятися, оскільки я не знаю, наскільки бар'єри пам'яті можуть унеможливити центральні процесори не виконувати замовлення.

Джерела:


0

Поки я працював над завантажуваним в Інтернеті відео-посібником з розробки 3D-графіки та ігрового движка, працюючи з сучасним OpenGL. Ми використовували volatileв одному з наших класів. Веб-сайт підручника можна знайти тут, а відео, що працює з volatileключовим словом, - у Shader Engineсерії 98. Ці роботи не є моїми власними, а акредитовані, Marek A. Krzeminski, MAScі це уривок зі сторінки завантаження відео.

"Оскільки тепер наші ігри можуть працювати в декількох потоках, важливо правильно синхронізувати дані між потоками. У цьому відео я покажу, як створити клас блокування volitile для забезпечення належної синхронізації змінних volitile ..."

І якщо ви підписані на його веб - сайт і мати доступ до його відео в межах цього відео він посилається на цю статтю щодо використання Volatileз multithreadingпрограмуванням.

Ось стаття за посиланням вище: http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766

мінливий: найкращий друг багатопоточного програміста

Андрій Александреску, 01 лютого 2001 р

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

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

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

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

Просто маленьке ключове слово

Хоча як стандарти C, так і C ++ помітно мовчать, коли справа стосується потоків, вони роблять невелику поступку багатопоточності у формі ключового слова volatile.

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

Розглянемо такий код:

class Gadget {
public:
    void Wait() {
        while (!flag_) {
            Sleep(1000); // sleeps for 1000 milliseconds
        }
    }
    void Wakeup() {
        flag_ = true;
    }
    ...
private:
    bool flag_;
};

Призначення Gadget :: Wait вище - перевіряти змінну-член flag_ щосекунди і повертати, коли для цієї змінної інший потік встановив значення true. Принаймні так задумав його програміст, але, на жаль, Чекати неправильно.

Припустимо, компілятор вияснив, що Sleep (1000) - це виклик зовнішньої бібліотеки, який не може змінити змінну-член flag_. Тоді компілятор приходить до висновку, що він може кешувати flag_ у регістрі та використовувати цей регістр замість доступу до повільнішої вбудованої пам'яті. Це відмінна оптимізація для однопотокового коду, але в цьому випадку це шкодить правильності: після того, як ви викликаєте Wait для якогось об'єкта Gadget, хоча інший потік викликає Wakeup, Wait буде циклічно назавжди. Це пояснюється тим, що зміна flag_ не відображатиметься в реєстрі, який кешує flag_. Оптимізація занадто ... оптимістична.

Кешування змінних у регістрах є дуже цінною оптимізацією, яка застосовується більшу частину часу, тому було б шкода її витратити. C та C ++ дають вам можливість явно вимкнути таке кешування. Якщо ви використовуєте мінливий модифікатор для змінної, компілятор не буде кешувати цю змінну в регістрах - кожен доступ буде вражати фактичне розташування цієї змінної в пам'яті. Отже, все, що вам потрібно зробити, щоб комбінований пристрій Wait / Wakeup працював, - це належним чином кваліфікувати flag_:

class Gadget {
public:
    ... as above ...
private:
    volatile bool flag_;
};

Більшість пояснень обґрунтування та використання енергозалежних зупиняються на цьому і радять вам визначити летючі класифікації примітивних типів, які ви використовуєте в декількох потоках. Однак можна зробити набагато більше з мінливими, оскільки це частина чудової системи типу C ++.

Використання мінливих з визначеними користувачем типами

Ви можете мінливо визначити не тільки примітивні типи, але й визначені користувачем типи. У цьому випадку volatile модифікує тип способом, подібним до const. (Ви також можете одночасно застосовувати const та volatile до одного типу.)

На відміну від const, volatile розрізняє примітивні типи та визначені користувачем типи. А саме, на відміну від класів, примітивні типи все ще підтримують всі свої операції (додавання, множення, присвоєння тощо), коли вони нестабільні. Наприклад, ви можете призначити енергонезалежний int для енергозалежного int, але ви не можете призначити енергонезалежний об’єкт для леткого об’єкта.

Давайте на прикладі проілюструємо, як мінлива робота працює на визначених користувачем типах.

class Gadget {
public:
    void Foo() volatile;
    void Bar();
    ...
private:
    String name_;
    int state_;
};
...
Gadget regularGadget;
volatile Gadget volatileGadget;

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

volatileGadget.Foo(); // ok, volatile fun called for
                  // volatile object
regularGadget.Foo();  // ok, volatile fun called for
                  // non-volatile object
volatileGadget.Bar(); // error! Non-volatile function called for
                  // volatile object!

Перехід з некваліфікованого типу на його летючий аналог є тривіальним. Однак, як і у випадку з const, ви не можете повернути поїздку з мінливої ​​в некваліфіковану. Ви повинні використовувати гіпс:

Gadget& ref = const_cast<Gadget&>(volatileGadget);
ref.Bar(); // ok

Клас, що відповідає витримці, надає доступ лише до підмножини свого інтерфейсу, підмножини, яка знаходиться під контролем реалізатора класу. Користувачі можуть отримати повний доступ до інтерфейсу цього типу лише за допомогою const_cast. Крім того, як і constness, мінливість поширюється від класу до його членів (наприклад, volatileGadget.name_ та volatileGadget.state_ є мінливими змінними).

мінливі, критичні розділи та умови перегонів

Найпростішим і найбільш часто використовуваним пристроєм синхронізації в багатопотокових програмах є мутекс. Мьютекс виставляє примітиви Acquire and Release. Як тільки ви викликаєте Acquire у якомусь потоці, будь-який інший потік, який викликає Acquire, заблокує. Пізніше, коли цей потік викликає Release, буде звільнений саме один потік, заблокований у виклику Acquire. Іншими словами, для даного мьютексу лише один потік може отримати час процесора між викликом Acquire та викликом Release. Код, що виконується між викликом Acquire та викликом Release, називається критичним розділом. (Термінологія Windows дещо заплутана, оскільки сама називає мьютекс критичним розділом, тоді як "мьютекс" насправді є міжпроцесним мьютексом. Було б непогано, якби їх назвали потоковим мьютексом і обробляють мутекс.)

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

Досвідчені багатопотокові програмісти, можливо, позіхнули, читаючи два абзаци вище, але їх метою є забезпечення інтелектуальних тренувань, тому що зараз ми будемо зв’язуватися з нестабільним зв’язком. Ми робимо це, проводячи паралель між світом типів C ++ та семантичним потоком потоків.

  • Поза критичним розділом будь-який потік може перервати будь-який інший у будь-який час; відсутній контроль, тому змінні, доступні з декількох потоків, є мінливими. Це відповідає початковому наміру volatile - запобігання компіляторові мимоволі кешування значень, що використовуються декількома потоками одночасно.
  • Усередині критичного розділу, визначеного мьютексом, доступ має лише один потік. Отже, всередині критичного розділу виконуваний код має однопоточну семантику. Контрольована змінна вже не мінлива - ви можете видалити мінливий кваліфікатор.

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

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

LockingPtr

Нам потрібен інструмент, який збирає отримання мьютексу та const_cast. Давайте розробимо шаблон класу LockingPtr, який ви ініціалізуєте за допомогою летючого об'єкта obj та mutex mtx. Протягом свого життя LockingPtr зберігає придбаний mtx. Крім того, LockingPtr пропонує доступ до нестабільного об'єкта. Доступ пропонується за допомогою розумних покажчиків через оператор-> та оператор *. Const_cast виконується всередині LockingPtr. Акторський склад є семантично дійсним, оскільки LockingPtr зберігає набраний мьютекс протягом усього життя.

Спочатку визначимо скелет класу Mutex, з яким LockingPtr буде працювати:

class Mutex {
public:
    void Acquire();
    void Release();
    ...    
};

Щоб використовувати LockingPtr, ви реалізуєте Mutex, використовуючи власні структури даних і примітивні функції вашої операційної системи.

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

Визначення LockingPtr дуже просте. LockingPtr реалізує нехитрий розумний вказівник. Він зосереджений виключно на зборі const_cast та критичного розділу.

template <typename T>
class LockingPtr {
public:
    // Constructors/destructors
    LockingPtr(volatile T& obj, Mutex& mtx)
      : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) {    
        mtx.Lock();    
    }
    ~LockingPtr() {    
        pMtx_->Unlock();    
    }
    // Pointer behavior
    T& operator*() {    
        return *pObj_;    
    }
    T* operator->() {   
        return pObj_;   
    }
private:
    T* pObj_;
    Mutex* pMtx_;
    LockingPtr(const LockingPtr&);
    LockingPtr& operator=(const LockingPtr&);
};

Незважаючи на свою простоту, LockingPtr є дуже корисним допоміжним засобом для написання правильного багатопотокового коду. Ви повинні визначити об'єкти, які спільно використовуються між потоками, як нестабільні і ніколи не використовувати const_cast з ними - завжди використовуйте автоматичні об'єкти LockingPtr. Проілюструємо це на прикладі.

Скажімо, у вас є два потоки, які мають спільний векторний об’єкт:

class SyncBuf {
public:
    void Thread1();
    void Thread2();
private:
    typedef vector<char> BufT;
    volatile BufT buffer_;
    Mutex mtx_; // controls access to buffer_
};

Усередині функції потоку ви просто використовуєте LockingPtr, щоб отримати контрольований доступ до змінної елемента buffer_:

void SyncBuf::Thread1() {
    LockingPtr<BufT> lpBuf(buffer_, mtx_);
    BufT::iterator i = lpBuf->begin();
    for (; i != lpBuf->end(); ++i) {
        ... use *i ...
    }
}

Код дуже легко написати і зрозуміти - коли вам потрібно використовувати buffer_, ви повинні створити LockingPtr, вказуючи на нього. Після цього ви отримаєте доступ до всього інтерфейсу вектора.

Приємна частина полягає в тому, що якщо ви помилитеся, компілятор вкаже на це:

void SyncBuf::Thread2() {
    // Error! Cannot access 'begin' for a volatile object
    BufT::iterator i = buffer_.begin();
    // Error! Cannot access 'end' for a volatile object
    for ( ; i != lpBuf->end(); ++i ) {
        ... use *i ...
    }
}

Ви не можете отримати доступ до будь-якої функції buffer_, доки не застосуєте const_cast або не використаєте LockingPtr. Різниця полягає в тому, що LockingPtr пропонує упорядкований спосіб застосування const_cast до змінних змінних.

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

unsigned int SyncBuf::Size() {
return LockingPtr<BufT>(buffer_, mtx_)->size();
}

Повернутися до примітивних типів

Ми побачили, наскільки мінлива мінливість захищає об’єкти від неконтрольованого доступу та як LockingPtr забезпечує простий та ефективний спосіб написання безпечного для потоку коду. Повернемось тепер до примітивних типів, які по-різному трактуються мінливими.

Давайте розглянемо приклад, коли кілька потоків мають спільну змінну типу int.

class Counter {
public:
    ...
    void Increment() { ++ctr_; }
    void Decrement() { —ctr_; }
private:
    int ctr_;
};

Якщо Increment і Decrement потрібно викликати з різних потоків, фрагмент вище є баггі. По-перше, ctr_ повинен бути мінливим. По-друге, навіть така, здавалося б, атомна операція, як ++ ctr_, насправді є триступеневою. Сама пам’ять не має арифметичних можливостей. При збільшенні змінної процесор:

  • Зчитує цю змінну в регістрі
  • Збільшує значення в реєстрі
  • Записує результат назад у пам’ять

Ця триступенева операція називається RMW (Read-Modify-Write). Під час модифікованої частини операції RMW більшість процесорів звільняє шину пам'яті, щоб надати іншим процесорам доступ до пам'яті.

Якщо в той час інший процесор виконує операцію RMW з тією ж змінною, ми маємо расову умову: друге записування перезаписує ефект першого.

Щоб цього уникнути, можна знову покластися на LockingPtr:

class Counter {
public:
    ...
    void Increment() { ++*LockingPtr<int>(ctr_, mtx_); }
    void Decrement() { —*LockingPtr<int>(ctr_, mtx_); }
private:
    volatile int ctr_;
    Mutex mtx_;
};

Тепер код правильний, але його якість поступається порівняно з кодом SyncBuf. Чому? Оскільки за допомогою Counter компілятор не попередить вас, якщо ви помилково отримаєте безпосередній доступ до ctr_ (не блокуючи його). Компілятор компілює ++ ctr_, якщо ctr_ мінливий, хоча згенерований код просто неправильний. Компілятор більше не є вашим союзником, і лише ваша увага може допомогти вам уникнути расових умов.

Що тоді робити? Просто інкапсулюйте примітивні дані, які ви використовуєте в структурах вищого рівня, і використовуйте летючі елементи з цими структурами. Парадоксально, але гірше використовувати volatile безпосередньо з вбудованими, незважаючи на те, що спочатку це було метою використання volatile!

мінливі функції члена

Дотепер у нас були класи, які об’єднують мінливі члени даних; Тепер давайте подумаємо про розробку класів, які, в свою чергу, будуть частиною більших об'єктів і будуть спільними між потоками. Ось де мінливі функції членів можуть бути корисними.

Розробляючи свій клас, ви змінюєте кваліфікацію лише тих функцій-членів, які є безпечними для потоків. Ви повинні припустити, що зовнішній код у будь-який час викликатиме летючі функції з будь-якого коду. Не забувайте: volatile дорівнює вільному багатопотоковому коду і відсутність критичного розділу; енергонезалежний дорівнює однопоточному сценарію або всередині критичного перерізу.

Наприклад, ви визначаєте віджет класу, який реалізує операцію у двох варіантах - потокобезпечному та швидкому, незахищеному.

class Widget {
public:
    void Operation() volatile;
    void Operation();
    ...
private:
    Mutex mtx_;
};

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

При реалізації летючої функції члена, перша операція, як правило, полягає у блокуванні цього за допомогою LockingPtr. Потім робота виконується за допомогою енергонезалежного брата або сестри:

void Widget::Operation() volatile {
    LockingPtr<Widget> lpThis(*this, mtx_);
    lpThis->Operation(); // invokes the non-volatile function
}

Резюме

Створюючи багатопотокові програми, ви можете використовувати енергонезалежні для своєї переваги. Ви повинні дотримуватися наступних правил:

  • Визначте всі спільні об’єкти як нестабільні.
  • Не використовуйте летючі речовини безпосередньо з примітивними типами.
  • Визначаючи спільні класи, використовуйте летючі функції-члени, щоб виразити безпеку потоків.

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

Кілька проектів, в яких я брав участь, з великим ефектом використовують volatile та LockingPtr. Код чистий і зрозумілий. Я пам’ятаю пару тупикових ситуацій, але я віддаю перевагу тупиковим ситуаціям, аніж гоночним умовам, оскільки їх набагато легше налагодити. Проблем, пов'язаних з умовами перегонів, практично не було. Але тоді ніколи не знаєш.

Подяки

Велике спасибі Джеймсу Канзе та Соріну Цзяну, які допомогли з проникливими ідеями.


Андрій Александреску - менеджер з розвитку компанії RealNetworks Inc. (www.realnetworks.com), що базується в Сіетлі, штат Вашингтон, та автор відомої книги "Сучасний дизайн C ++". З ним можна зв’язатися за адресою www.moderncppdesign.com. Андрій також є одним із найкращих викладачів семінару C ++ (www.gotw.ca/cpp_seminar).

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


0

Ключове слово volatileпо суті означає, що читання та запис об'єкта повинно виконуватися саме так, як написано програмою, і ніяк не оптимізовано . Бінарний код повинен слідувати коду C або C ++: завантаження, де це читається, магазин, де є запис.

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

volatile int i;
i = 1;
int j = i; 
if (j == 1) // not assumed to be true

volatileможе бути найважливішим інструментом у наборі інструментів "C - це мова асемблерів високого рівня" .

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

Атомні примітиви забезпечують приємний вигляд об'єктів для багатопоточності на вищому рівні, що полегшує міркування щодо коду. Майже всі програмісти повинні використовувати або атомарні примітиви, або примітиви, які забезпечують взаємне виключення, такі як мьютекси, блокування читання-запису, семафори або інші блокуючі примітиви.

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