Для чого volatile
потрібен C? Для чого він використовується? Що це зробить?
Для чого volatile
потрібен C? Для чого він використовується? Що це зробить?
Відповіді:
Volatile вказує компілятору не оптимізувати нічого, що має відношення до змінної змінної.
Існує щонайменше три поширені причини його використання, всі пов'язані із ситуаціями, коли значення змінної може змінюватися без дії із видимого коду: Коли ви взаємодієте з обладнанням, яке змінює саме значення; коли працює інший потік, який також використовує змінну; або коли є обробник сигналу, який може змінити значення змінної.
Скажімо, у вас є невелике обладнання, яке десь відображено в оперативній пам’яті і має дві адреси: порт команди та порт даних:
typedef struct
{
int command;
int data;
int isbusy;
} MyHardwareGadget;
Тепер ви хочете надіслати якусь команду:
void SendCommand (MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
Виглядає просто, але це може бути невдалим, оскільки компілятор вільний змінити порядок запису даних та команд. Це призведе до того, що наш маленький гаджет видасть команди з попереднім значенням даних. Також погляньте на цикл очікування, поки зайнятий. Це буде оптимізовано. Компілятор спробує бути розумним, прочитає значення isbusy лише один раз, а потім перейде в нескінченний цикл. Це не те, чого ти хочеш.
Спосіб подолати це - оголосити гаджет вказівника нестабільним. Таким чином компілятор змушений робити те, що ви написали. Він не може видалити призначення пам'яті, не може кешувати змінні в регістрах, а також не може змінити порядок призначення:
Це правильна версія:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
volatile
в C фактично виникло з метою автоматичного кешування значень змінної. Він скаже компілятору не кешувати значення цієї змінної. Таким чином, він буде генерувати код, щоб приймати значення даної volatile
змінної з основної пам'яті кожного разу, коли вона стикається з нею. Цей механізм використовується тому, що в будь-який час значення може бути змінено ОС або будь-яке переривання. Тож використання volatile
допоможе нам щоразу отримувати доступ до значення.
volatile
полягала в тому, щоб компілятори могли оптимізувати код, все ж дозволяючи програмістам досягати тієї семантики, яку можна було б досягти без таких оптимізацій. Автори Стандарду очікували, що реалізація якості підтримуватиме будь-яку корисну семантику з огляду на їх цільові платформи та поля застосувань, і не сподівалися, що автори-компілятори намагатимуться запропонувати семантику найнижчої якості, що відповідає Стандарту і не була б 100% дурний (зауважимо, що автори Стандарту явно визнають в обґрунтуванні ...
Ще одне використання volatile
- обробники сигналів. Якщо у вас є такий код:
int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
Компілятору дозволено помітити, що тіло циклу не торкається quit
змінної і перетворює цикл у while (true)
цикл. Навіть якщо quit
змінна встановлена на обробці сигналів для SIGINT
і SIGTERM
; компілятор не може цього знати.
Однак якщо оголошена quit
змінна volatile
, компілятор змушений завантажувати її кожен раз, тому що вона може бути змінена в іншому місці. Це саме те, що ви хочете в цій ситуації.
quit
, компілятор може оптимізувати його в постійний цикл, припускаючи що немає можливості quit
змінитись між ітераціями. NB: Це не обов'язково є доброю заміною фактичного програмування безпечних потоків.
volatile
або інших маркерів, передбачається, що нічого, що знаходиться поза циклом, не змінює цю змінну, як тільки вона потрапляє в цикл, навіть якщо це глобальна змінна.
extern int global; void fn(void) { while (global != 0) { } }
з gcc -O3 -S
і подивитися на отриманий файл збірки, на моїй машині це робить movl global(%rip), %eax
; testl %eax, %eax
; je .L1
; .L4: jmp .L4
, тобто нескінченний цикл, якщо глобальний не дорівнює нулю. Потім спробуйте додати volatile
і побачити різницю.
volatile
повідомляє компілятору, що ваша змінна може бути змінена іншим способом, ніж код, який має доступ до неї. наприклад, це може бути розташування пам'яті, відображене вводу / виводу. Якщо це не вказано в таких випадках, деякі змінні доступу можуть бути оптимізовані, наприклад, його вміст може зберігатися в реєстрі, а місце пам'яті знову не зчитуватися.
Дивіться цю статтю Андрія Олександреску, " мінливий - кращий друг багатопоточного програміста "
Летюча ключове слово було розроблено , щоб запобігти оптимізацію компілятора , який може позбавити код неправильного в присутності деяких асинхронних подій. Наприклад, якщо ви оголосите примітивну змінну як летючі , компілятор не допускається кешувати його в регістрі - загальна оптимізацію , яка буде мати катастрофічні наслідки, якщо ці змінне були розділені між декількома потоками. Таким чином , загальне правило, якщо у вас є змінні примітивного типу , які повинні бути розділені між декількома потоками, оголосити ці змінні летючий. Але ви можете зробити набагато більше з цим ключовим словом: ви можете використовувати його для лову коду, який не є безпечним для потоків, і це можна зробити під час компіляції. У цій статті показано, як це робиться; рішення включає простий розумний покажчик, який також спрощує серіалізацію критичних розділів коду.
Стаття стосується як C
і C++
.
Також дивіться статтю " С ++ та небезпеки подвійного перевірки блокування " Скотта Мейерса та Андрея Олександреску:
Тому, маючи справу з деякими місцями пам’яті (наприклад, портів, відображених у пам'яті, або пам’яті, на яку посилаються ISR [Рутини переривання служби]), деякі оптимізації потрібно призупинити. мінливий існує для визначення спеціальної обробки для таких локацій, зокрема: (1) вміст летючої змінної є "нестабільним" (може змінитись невідомим компілятору), (2) усі записи на летючі дані є "помітними", тому вони повинні бути виконані релігійно, і (3) всі операції з летучими даними виконуються в тій послідовності, в якій вони відображаються у вихідному коді. Перші два правила забезпечують правильне читання та письмо. Останній дозволяє реалізувати протоколи вводу / виводу, що змішують вхід і вихід. Офіційно це нестабільні гарантії C та C ++.
volatile
не гарантує атомність.
Моє просте пояснення:
У деяких сценаріях, заснованих на логіці чи коді, компілятор буде робити оптимізацію змінних, які, на його думку, не змінюються. У volatile
запобігає ключове слово змінної оптимізується.
Наприклад:
bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
// execute logic for the scenario where the USB isn't connected
}
З наведеного вище коду компілятор може подумати usb_interface_flag
, що визначено як 0, і що в циклі while він назавжди буде нульовим. Після оптимізації компілятор буде ставитись до цього як до while(true)
всього, що призводить до нескінченного циклу.
Щоб уникнути подібних сценаріїв, ми оголошуємо прапор мінливим, ми повідомляємо компілятору, що це значення може бути змінено зовнішнім інтерфейсом або іншим модулем програми, тобто, будь ласка, не оптимізуйте його. Ось такий варіант використання для енергонезалежних.
Граничне використання для летючих є наступним. Скажімо, ви хочете обчислити числову похідну функції f
:
double der_f(double x)
{
static const double h = 1e-3;
return (f(x + h) - f(x)) / h;
}
Проблема полягає в тому, що, x+h-x
як правило, не доводиться h
через помилки округлення. Подумайте над цим: коли ви підбираєте дуже близькі числа, ви втрачаєте багато значущих цифр, які можуть зіпсувати обчислення похідної (подумайте 1.00001 - 1). Можливе рішення може бути
double der_f2(double x)
{
static const double h = 1e-3;
double hh = x + h - x;
return (f(x + hh) - f(x)) / hh;
}
але залежно від комутаторів вашої платформи та компілятора, другий рядок цієї функції може бути знищений агресивно оптимізуючим компілятором. Так ви пишете замість цього
volatile double hh = x + h;
hh -= x;
змусити компілятора прочитати місце пам'яті, що містить hh, втрачаючи можливу можливість оптимізації.
h
або hh
похідна формула? Коли hh
обчислюється, остання формула використовує її як першу, без різниці. Може, так і має бути (f(x+h) - f(x))/hh
?
h
і hh
полягає в тому hh
, що операція прирізана до деякої негативної сили двох x + h - x
. У цьому випадку x + hh
і x
різняться точно за hh
. Ви також можете взяти свою формулу, вона дасть той самий результат, оскільки x + h
і x + hh
є рівними (тут важливий знаменник).
x1=x+h; d = (f(x1)-f(x))/(x1-x)
? без використання летючого.
-ffast-math
еквівалентом.
Є два варіанти використання. Вони спеціально використовуються частіше при вбудованій розробці.
Компілятор не оптимізує функції, що використовують змінні, визначені за допомогою летючого ключового слова
Летючий використовується для доступу до точних місць пам’яті в оперативній пам’яті, ПЗУ і т. Д.… Це використовується частіше для управління пристроями, нанесеними на карту пам’яті, доступом до регістрів процесора та пошуку конкретних місць пам’яті.
Див. Приклади зі списком складання. Re: Використання C "летючого" ключового слова у вбудованому розвитку
Летючий також корисний, коли ви хочете змусити компілятор не оптимізувати певну кодову послідовність (наприклад, для написання мікро-еталону).
Я згадаю ще один сценарій, коли леткі речовини важливі.
Припустимо, ви складете карту пам'яті на файл для швидшого вводу / виводу, і цей файл може змінюватися за кадром (наприклад, файл не знаходиться на локальному жорсткому диску, а замість нього подається через мережу іншим комп'ютером).
Якщо ви отримуєте доступ до даних файлу, відображеної на пам'ять, через покажчики на енергонезалежні об'єкти (на рівні вихідного коду), то код, сформований компілятором, може отримувати ті самі дані кілька разів, не знаючи про це.
Якщо ці дані змінюються, ваша програма може перетворитись на дві або більше різних версій даних і перейти в невідповідний стан. Це може призвести не тільки до логічно неправильної поведінки програми, але і до експлуатаційних дірок у безпеці, якщо вона обробляє ненадійні файли або файли з недовірених місць.
Якщо ви дбаєте про безпеку, і вам слід, це важливий сценарій.
мінливий означає, що сховище може змінитися в будь-який час і змінитись, але щось поза контролем користувацької програми. Це означає, що якщо ви посилаєтесь на змінну, програма завжди повинна перевіряти фізичну адресу (тобто відображений вхідний файл fifo), а не використовувати її в кешованому вигляді.
Вікі говорять про все volatile
:
Документ ядра Linux також відзначає volatile
:
На мою думку, не слід чекати занадто багато від volatile
. Для ілюстрації подивіться на приклад у високоголосній відповіді Нілса Піпенбрінка .
Я б сказав, його приклад не підходить volatile
. volatile
використовується лише для:
запобігання компілятору робити корисні та бажані оптимізації . Це нічого не стосується безпечного потоку, атомного доступу або навіть порядку пам'яті.
У цьому прикладі:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
gadget->data = data
, Перш gadget->command = command
за все гарантується тільки в скомпільований код з допомогою компілятора. Під час роботи процесор все ще можливо переробляє порядок передачі даних та команд щодо архітектури процесора. Обладнання могло отримати неправильні дані (припустимо, гаджет відображається на апаратному вводу-виводу). Бар'єр пам'яті необхідний між даними та призначенням команд.
volatile
приниження продуктивності без причини. Щодо того, чи достатньо, це буде залежати від інших аспектів системи, про які програміст може знати більше, ніж компілятора. З іншого боку, якщо процесор гарантує, що інструкція запису на певну адресу буде змивати кеш процесора, але компілятор не надав ніякого способу очищення змінних кешованих регістрів, про які процесор нічого не знає, промивання кешу буде марним.
Мовою, розробленою Деннісом Річі, кожен доступ до будь-якого об'єкта, крім автоматичних об'єктів, адреса якого не була взята, веде себе так, ніби він обчислює адресу об'єкта, а потім читає або записує сховище за цією адресою. Це зробило мову дуже потужною, але сильно обмеженою.
Хоча можливо було б додати класифікатор, який запропонував би компілятору припустити, що певний об'єкт не буде змінено дивними способами, таке припущення було б доречним для переважної більшості об'єктів у програмах на C, і це було б було недоцільно додавати кваліфікатор до всіх об'єктів, для яких таке припущення було б доречним. З іншого боку, деяким програмам потрібно використовувати деякі об'єкти, для яких таке припущення не було б дотриманим. Щоб вирішити цю проблему, Стандарт говорить, що компілятори можуть припускати, що об'єкти не задекларованіvolatile
, не матимуть їхнього значення або не змінюватимуться способами, що знаходяться поза контролем компілятора, або будуть поза розумним розумінням компілятора.
Оскільки різні платформи можуть мати різні способи спостереження або зміни об'єктів за межами контролю компілятора, доцільно, щоб якісні компілятори для цих платформ відрізнялися точним керуванням volatile
семантикою. На жаль, оскільки стандарту не вдалося запропонувати, що якісні компілятори, призначені для програмування низького рівня на платформі, повинні обробляти volatile
таким чином, щоб розпізнавати будь-які і всі відповідні ефекти певної операції читання / запису на цій платформі, багато компіляторів не вистачає цього таким чином, таким чином, що ускладнює обробку таких речей, як фоновий введення / виведення, таким чином, який є ефективним, але не може бути порушений компіляційними "оптимізаціями".
Летучий елемент може бути змінений поза компільованим кодом (наприклад, програма може зіставити змінну змінну в регістр, нанесений на пам'ять.) Компілятор не застосовуватиме певні оптимізації до коду, який обробляє мінливу змінну - наприклад, вона виграла ' t завантажуйте його в регістр, не записуючи його в пам'ять. Це важливо при роботі з апаратними регістрами.
Як справедливо пропонують багато хто з них, популярне використання ключових ключових слів - це пропустити оптимізацію змінної змінної.
Найкраща перевага, яка спадає на думку, і яку варто згадати, прочитавши про непостійну, - запобігання відкоту змінної у випадку a longjmp
. Немісцевий стрибок.
Що це означає?
Це просто означає, що останнє значення буде збережене після того, як ви зробите розмотування стека , щоб повернутися до попереднього кадру стека; як правило, у випадку помилкового сценарію.
Оскільки це питання не виходить за межі цього питання, я не збираюся setjmp/longjmp
тут деталізувати , але про це варто прочитати; і як можна використовувати функцію мінливості для збереження останнього значення.