У C ++ 11 зазвичай ніколи не використовуються volatile
для нарізування різьби, лише для MMIO
Але TL: DR, він працює "як би атомний" mo_relaxed
на апаратному забезпеченні з когерентними кешами (тобто все); достатньо зупинити компілятори, які зберігають параметри в реєстрах. atomic
не потрібні бар'єри пам’яті для створення атомності чи видимості між потоками, лише для того, щоб поточний потік чекав до / після операції, щоб створити впорядкування між доступом цього потоку до різних змінних. mo_relaxed
ніколи не потребує жодних бар'єрів, просто завантажуйте, зберігайте або обробляйте RMW.
Для рулонного свого власного Атомікса з volatile
(і інлайн-асемблер для бар'єрів) в старі часи до C ++ 11 std::atomic
, volatile
був тільки хорошим способом отримати деякі речі , щоб працювати . Але це залежало від безлічі припущень про те, як реалізовувались роботи і ніколи не гарантувалися жодним стандартом.
Наприклад, ядро Linux все ще використовує власну ручну атоміку з volatile
, але підтримує лише декілька конкретних реалізацій C (GNU C, clang та, можливо, ICC). Частково це пов’язано з розширеннями GNU C та синтаксисом та семантикою вбудованої ASM, а також тому, що це залежить від деяких припущень щодо роботи компіляторів.
Майже завжди неправильний вибір нових проектів; ви можете використовувати std::atomic
(з std::memory_order_relaxed
), щоб отримати компілятор, щоб випромінювати той самий ефективний машинний код, що і ви volatile
. std::atomic
з mo_relaxed
застарілими volatile
для цілей різьблення. (за винятком можливо обійти помилки пропущеної оптимізації з atomic<double>
деякими компіляторами .)
Внутрішня реалізація std::atomic
основних компіляторів (наприклад, gcc та clang) не використовує лише volatile
внутрішньо; компілятори безпосередньо піддають атомному навантаженню, накопиченню та вбудованому в RMW функції. (наприклад, вбудовані GNU C,__atomic
які працюють на "звичайних" об'єктах.)
Леткі можна використовувати на практиці (але не робіть цього)
Це volatile
було застосовано на практиці для таких речей, як exit_now
прапор для всіх (?) Існуючих реалізацій C ++ на реальних процесорах, через те, як працюють центральні процесори (узгоджені кеші) та спільні припущення про те, як volatile
слід працювати. Але не багато іншого, і не рекомендується. Мета цієї відповіді - пояснити, як реально працюють існуючі процесори та C ++. Якщо вам це не байдуже, все, що вам потрібно знати, - це те, що std::atomic
застарілі mo_relaxed застаріли volatile
для нанизування різьби.
(Стандарт ISO C ++ щодо нього досить розпливчастий, просто кажучи, що volatile
доступ слід оцінювати строго відповідно до правил абстрактної машини C ++, а не оптимізувати його. Враховуючи, що реальні реалізації використовують адресний простір пам'яті машини для моделювання адресного простору C ++, це означає, що volatile
читання та завдання повинні компілюватися для завантаження / зберігання інструкцій для доступу до представлення об'єктів у пам'яті.)
Як вказується інша відповідь, exit_now
прапор - це простий випадок зв'язку між потоками, який не потребує синхронізації : не публікується, що вміст масиву готовий чи щось подібне. Просто магазин, який негайно помітили неоптимізоване завантаження в іншій нитці.
// global
bool exit_now = false;
// in one thread
while (!exit_now) { do_stuff; }
// in another thread, or signal handler in this thread
exit_now = true;
Без мінливих або атомних, правило , як-ніби, і припущення про відсутність перегону даних UB дозволяє компілятору оптимізувати його в асм, який перевіряє прапор лише один раз , перед тим як вводити (чи ні) нескінченний цикл. Саме це і відбувається в реальному житті для справжніх компіляторів. (І зазвичай оптимізуємо велику частину, do_stuff
оскільки цикл ніколи не виходить, тому будь-який пізній код, який, можливо, використовував результат, недоступний, якщо ми введемо цикл).
// Optimizing compilers transform the loop into asm like this
if (!exit_now) { // check once before entering loop
while(1) do_stuff; // infinite loop
}
Програма багатопотокової роботи, що застрягла в оптимізованому режимі, але працює нормально в -00, є прикладом (з описом виходу ASM GCC) того, як саме це відбувається з GCC на x86-64. Також програмування MCU - оптимізація C ++ O2 перервана під час циклу на електроніці.SE показує ще один приклад.
Ми, як правило, хочемо агресивних оптимізацій, які CSE та підйомник завантажує з циклів, в тому числі для глобальних змінних.
До C ++ 11 volatile bool exit_now
був один із способів зробити цю роботу за призначенням (у звичайних C ++ реалізаціях). Але в C ++ 11 UB-перегони даних все ще застосовуються до цього, volatile
тому він фактично не гарантується стандартом ISO для роботи скрізь, навіть якщо припускати, що HW є когерентними кешами.
Зауважте, що для ширших типів volatile
не дає гарантії відсутності сліз. Я ігнорував це відмінність, bool
оскільки це не проблема в звичайних реалізаціях. Але це також є частиною того, чому volatile
все ще підлягає UB-перегоні даних, а не еквівалентні розслабленому атомному.
Зауважте, що "за призначенням" не означає, що нитка робить exit_now
очікування, поки інший потік фактично вийде. Або навіть, що він чекає, коли енергонезалежний exit_now=true
сховище стане навіть глобально видимим, перш ніж продовжувати наступні операції в цій темі. ( atomic<bool>
за замовчуванням mo_seq_cst
змусить зачекати, перш ніж пізніше буде завантажено seq_cst. У багатьох ISA ви просто отримаєте повний бар'єр після магазину).
C ++ 11 забезпечує не-UB спосіб, який компілює той самий
А «продовжує працювати» або «вихід» Тепер прапор слід використовувати std::atomic<bool> flag
зmo_relaxed
Використання
flag.store(true, std::memory_order_relaxed)
while( !flag.load(std::memory_order_relaxed) ) { ... }
дасть вам таку саму зону (без дорогих інструкцій щодо бар'єру), від якої ви отримаєте volatile flag
.
Крім того, що не рветься, atomic
також дає можливість зберігати в одному потоці та завантажувати в інший без UB, тому компілятор не може підняти навантаження з циклу. (Припущення про відсутність перегонів з передачею даних - це те, що дозволяє робити агресивні оптимізації для неатомних енергонезалежних об'єктів.) Ця особливість atomic<T>
майже однакова, як volatile
і для чистих навантажень та чистих сховищ.
atomic<T>
також здійснюйте +=
операції з атомною РМЗ (значно дорожче, ніж атомний навантаження в тимчасовий, експлуатуйте, то окремий атомний склад. Якщо ви не хочете атомної RMW, напишіть свій код з місцевим тимчасовим).
Отримавши seq_cst
замовлення за замовчуванням, яке ви отримаєте while(!flag)
, воно також додає гарантії замовлення wrt. неатомарний доступ і до інших атомних доступу.
(Теоретично стандарт ISO C ++ не виключає оптимізацію атоміки під час компіляції. Але на практиці компілятори цього не роблять, оскільки немає способу контролю, коли це не буде нормально. Є кілька випадків, коли навіть volatile atomic<T>
не може бути Буде достатньо контролю над оптимізацією атомів, якщо компілятори зробили оптимізацію, так що зараз компілятори не дивляться. Дивіться, чому компілятори не зливають зайве std :: atomic пише? Зауважте, що wg21 / p0062 рекомендує не використовувати volatile atomic
в поточному коді захист від оптимізації атомія.)
volatile
насправді працює для цього на реальних процесорах (але все одно не використовуйте)
навіть із слабко упорядкованими моделями пам'яті (не-x86) . Але на самому ділі не використовувати його, використовувати atomic<T>
з mo_relaxed
замість !! Суть цього розділу полягає у вирішенні помилкових уявлень про те, як працюють реальні процесори, а не для виправдання volatile
. Якщо ви пишете безблочний код, ви, ймовірно, дбаєте про продуктивність. Розуміння кеш-пам'яті та витрат на взаємодію між потоками, як правило, важливо для хорошої роботи.
Реальні процесори мають когерентні кеші / спільну пам'ять: після того, як сховище з одного ядра стане глобально видимим, жодне інше ядро не може завантажити чергове значення. (Див. Також Міфи Програмісти вірять у кеші процесора, які розповідають про летучі Java, еквівалентні C ++ atomic<T>
з порядком пам'яті seq_cst.)
Коли я кажу навантаження , я маю на увазі інструкцію ASM, яка отримує доступ до пам'яті. Ось що volatile
забезпечує доступ, і це не те саме, що перетворення lvalue в rvalue неатомної / енергонезалежної змінної C ++. (наприклад, local_tmp = flag
або while(!flag)
).
Єдине, що вам потрібно перемогти - це оптимізація часу компіляції, яка взагалі не завантажується після першої перевірки. Будь-яке завантаження + перевірка кожної ітерації є достатньою, без будь-якого замовлення. Без синхронізації між цією ниткою та основною ниткою не має сенсу говорити про те, коли саме відбулося зберігання, чи впорядкування завантаження wrt. інші операції в циклі. Тільки коли це видно в цій темі, це важливо. Коли ви бачите встановлений прапор exit_now, ви виходите. Затримка між ядрами на типовому X86 Xeon може бути приблизно як 40ns між окремими фізичними ядрами .
Теоретично: C ++ потоки на апаратному забезпеченні без узгоджених кешів
Я не бачу, що це може бути віддалено ефективним, просто чистий ISO C ++, не вимагаючи від програміста явних помилок у вихідному коді.
Теоретично у вас може бути реалізація C ++ на машині, яка не була такою, що вимагає явних флеш-сигналів, згенерованих компілятором, щоб зробити видимими інші потоки на інших ядрах . (Або для читання, щоб не використовувати копію, можливо, несвіжу). Стандарт C ++ не робить цього неможливим, але модель пам'яті C ++ розроблена так, щоб бути ефективною на когерентних машинах спільної пам'яті. Наприклад, стандарт C ++ навіть говорить про "узгодженість читання-читання", "когерентність читання-читання" тощо. Одна примітка в стандарті навіть вказує на підключення до обладнання:
http://eel.is/c++draft/intro.races#19
[Примітка: чотири попередні вимоги до узгодженості ефективно забороняють упорядкування атомних операцій компілятором на один об'єкт, навіть якщо обидві операції є навантаженими розслабленими. Це ефективно забезпечує гарантію узгодженості кешу, яку надає більшість апаратних засобів, доступних для атомних операцій C ++. - кінцева примітка]
Немає механізму, щоб release
магазин зберігав лише очищення себе та декілька вибраних діапазонів адрес: він повинен був би синхронізувати все, оскільки він не знав би, які інші потоки можуть хотіти читати, якби їхнє завантаження бачило цей випуск-сховище (утворюючи послідовність випуску, яка встановлює взаємозв'язок, що відбувається перед потоками, гарантуючи, що раніше неатомні операції, виконані потоком запису, тепер безпечні для читання. Якщо тільки це не запише далі після сховища релізів ...) Або компілятори мали б щоб бути по- справжньому розумним, щоб довести, що лише кілька кеш-рядків потрібно промити.
Пов’язано: моя відповідь на те, чи безпечно Mov + mfence на NUMA? детально розповідається про відсутність систем x86 без когерентної спільної пам'яті. Також пов’язано: Вантажі та магазини, що упорядковують ARM для отримання додаткових відомостей про вантажі / магазини в одному місці.
Є , я думаю, кластери з некогерентною спільною пам'яттю, але вони не є системами одного зображення. Кожен домен когерентності працює окремим ядром, тому ви не можете запускати нитки однієї програми C ++ через нього. Натомість ви запускаєте окремі екземпляри програми (у кожного з їх власним адресним простором: вказівники в одному екземплярі недійсні в іншому).
Щоб змусити їх спілкуватися один з одним за допомогою явних флешів, зазвичай ви використовуєте MPI або інший API для передачі повідомлень, щоб програма визначала, які діапазони адрес потрібно промивати.
Справжнє обладнання не працює std::thread
через межі когерентності кешу:
Існують деякі асиметричні ARM-мікросхеми, що мають спільний фізичний адресний простір, але не керовані домени кешу. Тож не злагоджені. (наприклад, потік коментаря до ядра A8 та Cortex-M3, як TI Sitara AM335x).
Але різні ядра працювали б на цих ядрах, а не єдине зображення системи, яке могло б запускати потоки по обидва ядра. Мені невідомі будь-які C ++ реалізації, які виконують std::thread
потоки в ядрах процесора без узгоджених кешів.
Зокрема для ARM, GCC та кланг генерують код, припускаючи, що всі потоки працюють у тому самому внутрішньообмінному домені. Насправді йдеться в посібнику ISA ARMv7
Ця архітектура (ARMv7) написана з очікуванням, що всі процесори, що використовують одну операційну систему або гіпервізор, знаходяться в одній домі Внутрішньої спільної доступності
Таким чином, несумісна спільна пам'ять між окремими доменами є лише справою для явного використання специфічних для системи областей спільної пам'яті для зв'язку між різними процесами під різними ядрами.
Дивіться також цю дискусію в CoreCLR про використання кодового родуdmb ish
(Внутрішній обмінний бар'єр) проти dmb sy
(Система) бар'єрів пам'яті у цьому компіляторі.
Я стверджую, що жодна реалізація C ++ для інших інших ISA не std::thread
перетинає ядра з некогерентними кешами. У мене немає доказів того, що такої реалізації не існує, але це здається дуже малоймовірним. Якщо ви не орієнтуєтесь на певну екзотичну частину HW, яка працює таким чином, ваше мислення щодо продуктивності повинно припускати MESI-подібність кеш-пам'яті між усіма потоками. ( atomic<T>
Однак переважно використовувати способи, які гарантують правильність!)
Узгоджені кеші роблять його простим
Але для багатоядерної системи з когерентними кешами реалізація сховища випусків просто означає замовлення фіксації в кеш для сховищ цього потоку, не роблячи явного очищення. ( https://preshing.com/20120913/acquire-and-release-semantics/ та https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ ). (А придбання-завантаження означає замовлення доступу до кешу в іншому ядрі).
Інструкція щодо бар'єру пам’яті просто блокує завантаження поточного потоку та / або зберігання поточного потоку до тих пір, поки буфер магазину не зливеться; це завжди відбувається якомога швидше самостійно. ( Чи гарантує бар'єр пам’яті те, що кохерентність кешу завершена? Вирішує це неправильне уявлення). Тож якщо вам не потрібно замовляти, просто підкажіть видимість в інших потоках, mo_relaxed
це добре. (Так і є volatile
, але не робіть цього.)
Див. Також C / C ++ 11 відображень для процесорів
Приємний факт: на x86 кожен магазин ASM є сховищем випусків, оскільки модель пам'яті x86 в основному є seq-cst плюс буфер зберігання (з переадресацією магазину).
Напівзалежно від повторного зберігання буфера, глобальної видимості та когерентності: C ++ 11 гарантує дуже мало. Більшість реальних ISA (крім PowerPC) гарантують, що всі потоки можуть узгодити порядок появи двох магазинів двома іншими потоками. (У формальній термінології з моделями пам'яті комп'ютерної архітектури вони "атомні з декількома копіями").
Ще одне неправильне уявлення полягає в тому, що інструкції щодо заповнення пам’яті для забору пам’яті потрібні, щоб промити буфер магазину для інших ядер, щоб взагалі побачити наші магазини . Насправді буфер магазину завжди намагається максимально швидко злити (скористатися кешем L1d), інакше він заповнить і зупинить виконання. Що повний бар'єр / огорожа робить - це затримка поточної нитки, поки буфер магазину не виснажений , тому наші наступні вантажі з’являються в глобальному порядку після наших попередніх магазинів.
(сильно впорядкована модель пам'яті ASM означає, що volatile
на x86 може зблизити вас mo_acq_rel
, за винятком того, що перезапис часу компіляції з неатомними змінними все ще може відбутися. Але більшість тих, хто не має x86, мають слабко упорядковані моделі пам'яті, volatile
і relaxed
приблизно так слабкий, як mo_relaxed
дозволяє.)