Чи гарантує "непостійний" взагалі що-небудь в портативному коді С для багатоядерних систем?


12

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

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

Серед інших проблем:

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

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

ВИКОРИСТАННЯ 1
Єдиним широко узгодженим використанням летучих, здається, є для старих або вбудованих систем, де певні місця пам'яті апаратно відображаються на функції вводу / виводу, як біт у пам'яті, що управляє (безпосередньо в апаратному забезпеченні) світлом , або трохи пам’яті, яка вказує на те, чи клавіша клавіатури відключена чи ні (тому що вона підключена апаратним забезпеченням безпосередньо до клавіші).

Здається, що "використання 1" не відбувається в портативному коді, цілі якого включають багатоядерні системи.

ВИКОРИСТАННЯ 2
Не надто відрізняється від "використання 1" - це пам'ять, яку в будь-який час можна прочитати або записати обробником переривання (який може керувати світлом або зберігати інформацію від ключа). Але вже для цього ми маємо проблему, що залежно від системи обробник переривання може працювати на іншому ядрі з власним кешем пам'яті , а "непостійний" не гарантує когерентності кешу у всіх системах.

Тож "використання 2", здається, виходить за рамки того, що може досягти "мінливий".

ВИКОРИСТАННЯ 3
Єдине інше безперечне використання, яке я бачу, - це запобігати неправильній оптимізації доступу через різні змінні, що вказують на ту саму пам'ять, яку компілятор не усвідомлює, - це та сама пам'ять. Але це, мабуть, лише беззаперечно, тому що люди не говорять про це - я бачив лише одну згадку про це. І я подумав, що стандарт C вже визнав, що "різні" покажчики (наприклад, різні аргументи на функцію) можуть вказувати на один і той же елемент або сусідні елементи, і вже вказав, що компілятор повинен створювати код, який працює навіть у таких випадках. Однак я не зміг швидко знайти цю тему в останньому (500 сторінок!) Стандарті.

Тож "використання 3" можливо взагалі не існує ?

Звідси моє запитання:

Чи гарантує "непостійний" взагалі що-небудь в портативному коді С для багатоядерних систем?


EDIT - оновлення

Після перегляду останнього стандарту виглядає так, що відповідь принаймні дуже обмежений так:
1. Стандарт багаторазово визначає спеціальну обробку для конкретного типу "летюча sig_atomic_t". Однак стандарт також говорить, що використання сигнальної функції в багатопотоковій програмі призводить до невизначеної поведінки. Тож цей випадок використання здається обмеженим зв’язком між однопотоковою програмою та її обробником сигналу.
2. Стандарт також визначає чітке значення для "летючого" стосовно setjmp / longjmp. (Приклад коду, де це має значення, наведено в інших питаннях та відповідях .)

Тож стає більш точним питанням:
чи гарантує "непостійний" взагалі що-небудь у переносному коді С для багатоядерних систем, крім (1), що дозволяє однопотоковій програмі отримувати інформацію від свого обробника сигналів, або (2) дозволяє setjmp код, щоб побачити змінні, змінені між setjmp та longjmp?

Це все ще так / ні питання.

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


3
Сигнали існують у портативному С; як щодо глобальної змінної, оновленої обробником сигналу? Це повинно бути, volatileщоб повідомити програму, що вона може змінюватися асинхронно.
Нейт

2
@NateEldredge Global, хоча сам по собі нестабільний, недостатньо хороший. Це також має бути атомним.
Євген Ш.

@EugeneSh .: Так, звичайно. Але питання, про volatileяке йде мова, стосується конкретно, що я вважаю необхідним.
Нейт

", хоча узгодження з кешем L1 не гарантує нічого більше стосовно координації з іншими потоками " Де "координація з кешем L1" недостатня для зв'язку з іншими потоками?
curiousguy

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

Відповіді:


1

Підсумовуючи проблему, виявляється (багато читаючи), що "мінливий" гарантує щось на кшталт: Значення буде прочитане / записане не просто з / в реєстр, а принаймні до кеш-пам'яті L1 ядра в тому ж порядку, що читання / запис відображається в коді .

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

Якби це було, тоді мінливі були б цілком непогані для змінних, якими ділиться кілька потоків, тому що впорядкування подій у кеші L1 - все, що вам потрібно зробити в типовому процесорі (тобто або багатоядерному, або багатопроцесорному на материнській платі), здатному співпрацювати таким чином, що дозволяє нормальна реалізація багатопотокового читання C / C ++ або Java з типовими очікуваними витратами (тобто, не величезними витратами на більшість атомних або невдоволених операцій з мютекс).

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

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

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

  • ви можете використовувати ptrace(механізм, подібний до ptrace) для встановлення значущих точок розриву в точках послідовності після операцій, що включають летючі об'єкти: ви дійсно можете зламати саме в цих точках (зауважте, що це працює лише в тому випадку, якщо ви готові встановити багато точок розриву як будь-які C / C ++ заяви можуть бути складені для багатьох різних початкових і кінцевих точок складання, як у масово розкрученому циклі);
  • в той час, як нитка виконання зупинених, ви можете прочитати значення всіх летучих об'єктів, оскільки вони мають своє канонічне подання (слідуючи ABI для відповідного типу); нестабільна локальна змінна може мати нетипове представлення, f.ex. зміщене подання: змінна, яка використовується для індексації масиву, може бути помножена на розмір окремих об'єктів, щоб полегшити індексацію; або він може бути замінений покажчиком на елемент масиву (до тих пір, поки всі використання змінної так само перетворені) (подумайте, змінивши dx на du в інтеграл);
  • Ви також можете змінювати ці об'єкти (доки відображення пам'яті дозволяє, оскільки мінливий об'єкт зі статичним терміном служби, який може бути кваліфікований, може знаходитись у діапазоні пам'яті, відображеному лише для читання).

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

Зауважимо, що повноцінна програма налагодження, яка враховує коливання всіх змінних, щонайменше, у точках послідовності, забезпечується режимом "нульової оптимізації" компілятора, режимом, який все ще виконує тривіальні оптимізації на зразок арифметичних спрощень (зазвичай не гарантовано немає оптимізація в усіх режимах). Але енергонезалежний сильніше, ніж неоптимізація: x-xможе бути спрощений для нелетучого цілого числа, xале не для летючого об'єкта.

Таким чином, мінливі засоби, які гарантовано будуть складені такими, як є , як і переклад з джерела на двійковий / збірник компілятором системного виклику, не є реінтерпретацією, зміненою або оптимізованою будь-яким чином компілятором. Зауважте, що виклики бібліотеки можуть бути або не бути системними викликами. Багато офіційних системних функцій - це фактично бібліотечні функції, які пропонують тонкий шар інтерпозиції і, як правило, відкладають ядро ​​в кінці. (Зокрема getpid, не потрібно переходити до ядра, і він може добре прочитати місце пам'яті, надане ОС, що містить інформацію.)

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

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

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

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

Загальний випадок - це те, що робить компілятор, коли він не має жодної інформації про конструкцію: f.ex. виклик віртуальної функції на значення на основі динамічної диспетчерики є загальним випадком, і прямий виклик на переорієнтовку після визначення в момент компіляції типу об'єкта, позначеного виразом, є конкретним випадком. У компіляторі завжди є загальна обробка справи всіх конструкцій, і це слід за ABI.

Летючий не робить нічого особливого для синхронізації потоків або надання «видимості пам’яті»: непостійний забезпечує лише гарантії на абстрактному рівні, видно зсередини потоку, який виконується або зупиняється, тобто всередині ядра CPU :

  • летючі нічого не говорять про те, які операції з пам'яттю досягають основної оперативної пам’яті (ви можете встановити конкретні типи кешування пам'яті за допомогою інструкцій по збірці або системних викликів для отримання цих гарантій);
  • непостійний не дає жодних гарантій щодо того, коли операції з пам'яттю будуть здійснені на будь-якому рівні кешу (навіть не L1) .

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

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

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


6

Я не експерт, але cppreference.com має те, що, як мені здається, є досить хорошою інформацієюvolatile . Ось суть цього:

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

Він також використовує:

Використання летких

1) статичні мінливі об'єкти моделюють пам'яті, нанесені на пам'ять порти вводу / виводу, і статичні const мінливі об'єкти моделюють вбудовані в пам'ять порти введення, такі як годинник в реальному часі

2) статичні летючі об'єкти типу sig_atomic_t використовуються для зв'язку з обробниками сигналів.

3) мінливі змінні, локальні для функції, що містить виклик макросу setjmp, - єдині локальні змінні, гарантовано зберігають свої значення після повернення longjmp.

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

І звичайно, він згадує, що volatileне корисно для синхронізації потоків:

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


2
Зокрема, (2) та (3) стосуються портативного коду.
Нейт

2
@TED ​​Незважаючи на доменне ім'я, посилання на інформацію про C, а не C ++
Девід Браун

@NateEldredge Ви рідко можете використовувати longjmpкод C ++.
curiousguy

@DavidBrown C і C ++ мають однакове визначення спостережуваної SE та по суті однакові примітивні нитки.
curiousguy

4

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

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

Незалежно від того, гарантує мова C мовлення щодо поведінки в пам'яті чи ні volatile, хоча особисто я вважаю, що мова зрозуміла. Спочатку ми маємо формальне визначення побічних ефектів, C17 5.1.2.3:

Доступ до volatileоб'єкта, зміна об'єкта, зміна файлу або виклик функції, яка виконує будь-яку з цих операцій, - це всі побічні ефекти , які є змінами в стані середовища виконання.

Стандарт визначає термін послідовності, як спосіб визначення порядку оцінки (виконання). Визначення формальне та громіздке:

Попередня послідовність - це асиметричне, перехідне, парне відношення між оцінками, виконаними однією ниткою, що викликає частковий порядок серед цих оцінок. З урахуванням будь-яких двох оцінок A і B, якщо A секвенується перед B, то виконання A передує виконанню B. (І навпаки, якщо A секвенується перед B, то B послідовно після A.) Якщо A не є послідовним до або після B, тоді A і B не є наслідком . Оцінки А і В невизначено секвенували , коли А секвеніруют або до , або після того, як B, але це не визначено which.13) Наявність точки послідовності між оцінкою виразів A і B випливає, що кожне обчислення значення і побічний ефект, пов'язаний з A, секвенуються перед кожним обчисленням значення та побічним ефектом, пов'язаним з B.

TL; DR вищезазначеного в основному полягає в тому, що у випадку, якщо ми маємо вираз, Aякий містить побічні ефекти, це потрібно зробити, виконуючи перед іншим виразом B, у випадку, якщо Bце відстежено після A.

Оптимізація коду С стає можливою завдяки цій частині:

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

Це означає, що програма може оцінювати (виконувати) вирази в тому порядку, який стандарт вимагає в іншому місці (порядок оцінювання тощо). Але йому не потрібно оцінювати (виконувати) значення, якщо воно може зробити висновок, що воно не використовується. Наприклад, операцію 0 * xне потрібно оцінювати xта просто замінювати вираз на 0.

Якщо доступ до змінної не є побічним ефектом. Тобто, у випадку , якщо він xє volatile, він повинен оцінити (виконати), 0 * xхоча результат завжди буде 0. Оптимізація не дозволена.

Крім того, стандарт говорить про поведінку, що спостерігається:

Найменшими вимогами щодо відповідної реалізації є:

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

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

Це означає, що в цьому прикладі

volatile int x;
volatile int y;
z = x;
z = y;

Обидва вирази призначення повинні бути оцінені і z = x; повинні бути оцінені раніше z = y;. Багатопроцесорна реалізація, яка передає ці дві операції на два різних ядра, що не мають наслідків, не відповідає!

Дилема полягає в тому, що компілятори не можуть робити багато в таких речах, як попереднє кешування та конвеєрне керування інструкціями тощо, особливо не під час роботи над версією ОС. І тому компілятори передають цю проблему програмістам, кажучи їм, що бар'єри пам’яті тепер відповідальність програміста. Хоча стандарт C чітко говорить, що проблему потрібно вирішити компілятором.

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


@curiousguy Не має значення.
Лундін

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

Якщо це просте нелетуче ціле число, чому б надлишкові записи zсправді виконувалися? (як z = x; z = y;) Значення буде видалено в наступному операторі.
curiousguy

@curiousguy Оскільки зчитування змінних змінних необхідно виконувати незалежно від зазначеної послідовності.
Лундін

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