Чому в С потрібна мінливість?


Відповіді:


423

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;
    }

46
Особисто я вважаю за краще, щоб цілий розмір був чіткістю, наприклад, int8 / int16 / int32 під час розмови з обладнанням. Хороша відповідь, хоча;)
tonylo

22
так, ви повинні оголосити речі з фіксованим розміром реєстру, але ей - це просто приклад.
Нілс Піпенбрінк

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

14
Детальніше читайте специфікацію C. Летючий має лише певну поведінку на вході / виводі пристрою, нанесеного на карту пам'яті, або на пам'ять, на яку торкається асинхронна функція переривання. Він нічого не говорить про потоки, і компілятор, який оптимізує доступ до пам'яті, торкається декількох потоків, є відповідним.
ефемієнт

17
@tolomea: абсолютно неправильно. сумно 17 осіб цього не знають. непостійний не є огорожею пам’яті. це пов'язано лише з уникненням елізії коду під час оптимізації, заснованої на припущенні невидимих ​​побічних ефектів .
v.oddou

187

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


Виникли? Чи не був "енергонезалежний" спочатку запозичений у C ++? Ну, я, мабуть, пам’ятаю ...
синтаксичний помилка

Це не мінливе все - це також забороняє деяке переупорядкування, якщо вказано як непостійне ..
FaceBro

4
@FaceBro: Мета volatileполягала в тому, щоб компілятори могли оптимізувати код, все ж дозволяючи програмістам досягати тієї семантики, яку можна було б досягти без таких оптимізацій. Автори Стандарду очікували, що реалізація якості підтримуватиме будь-яку корисну семантику з огляду на їх цільові платформи та поля застосувань, і не сподівалися, що автори-компілятори намагатимуться запропонувати семантику найнижчої якості, що відповідає Стандарту і не була б 100% дурний (зауважимо, що автори Стандарту явно визнають в обґрунтуванні ...
supercat

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

1
@syntaxerror: як його можна запозичити у C ++, коли C був на декаду старший за C ++ (як у перших релізах, так і в перших стандартах)?
phuclv

178

Ще одне використання volatile- обробники сигналів. Якщо у вас є такий код:

int quit = 0;
while (!quit)
{
    /* very small loop which is completely visible to the compiler */
}

Компілятору дозволено помітити, що тіло циклу не торкається quitзмінної і перетворює цикл у while (true)цикл. Навіть якщо quitзмінна встановлена ​​на обробці сигналів для SIGINTі SIGTERM; компілятор не може цього знати.

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


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

1
@AmitSinghTomar Це означає, що він говорить: Кожен раз, коли код перевіряє значення, він перезавантажується. В іншому випадку компілятору дозволено припустити, що функції, які не мають посилання на змінну, не можуть її змінити, тому припускаючи, що CesarB задумав, що вищевказаний цикл не встановлений quit, компілятор може оптимізувати його в постійний цикл, припускаючи що немає можливості quitзмінитись між ітераціями. NB: Це не обов'язково є доброю заміною фактичного програмування безпечних потоків.
підкреслюй_d

якщо quit є глобальною змінною, то компілятор не повинен оптимізувати цикл while, правильно?
П’єр Г.

2
@PierreG. Ні, компілятор завжди може вважати, що код є однопоточним, якщо не сказано інше. Тобто, за відсутності volatileабо інших маркерів, передбачається, що нічого, що знаходиться поза циклом, не змінює цю змінну, як тільки вона потрапляє в цикл, навіть якщо це глобальна змінна.
CesarB

1
@PierreG. Так, спробуйте, наприклад , складання 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і побачити різницю.
CesarB

60

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


30

Дивіться цю статтю Андрія Олександреску, " мінливий - кращий друг багатопоточного програміста "

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

Стаття стосується як Cі C++.

Також дивіться статтю " С ++ та небезпеки подвійного перевірки блокування " Скотта Мейерса та Андрея Олександреску:

Тому, маючи справу з деякими місцями пам’яті (наприклад, портів, відображених у пам'яті, або пам’яті, на яку посилаються ISR [Рутини переривання служби]), деякі оптимізації потрібно призупинити. мінливий існує для визначення спеціальної обробки для таких локацій, зокрема: (1) вміст летючої змінної є "нестабільним" (може змінитись невідомим компілятору), (2) усі записи на летючі дані є "помітними", тому вони повинні бути виконані релігійно, і (3) всі операції з летучими даними виконуються в тій послідовності, в якій вони відображаються у вихідному коді. Перші два правила забезпечують правильне читання та письмо. Останній дозволяє реалізувати протоколи вводу / виводу, що змішують вхід і вихід. Офіційно це нестабільні гарантії C та C ++.


Чи визначає стандарт, чи вважається читання "поведінкою, що спостерігається", якщо значення ніколи не використовується? Моє враження, що так і має бути, але коли я стверджував, що це було десь, хтось кинув мені виклик. Мені здається, що на будь-якій платформі, де зчитування мінливої ​​змінної могло б мати будь-який ефект, слід вимагати компілятора генерувати код, який виконує кожне вказане читання точно один раз; без цієї вимоги важко буде написати код, який створив передбачувану послідовність читання.
supercat

@supercat: Відповідно до першої статті, "Якщо ви використовуєте мінливий модифікатор змінної, компілятор не буде кешувати цю змінну в регістрах - кожен доступ буде впливати на фактичне місце пам'яті цієї змінної." Також у розділі § 6.7.3.6 стандарту c99 зазначено: "Об'єкт, який має тип летючого типу, може бути модифікований способами, невідомими для реалізації, або мати інші невідомі побічні ефекти". Далі випливає, що мінливі змінні можуть не зберігатись в кешах у регістрах, і що всі читання та записи повинні виконуватися в порядку відносно точок послідовності, щоб вони насправді були помітні.
Роберт С. Барнс

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

"компілятору заборонено кешувати його в регістрі" - Більшість архівних архітектур RISC ae register-машин, тому будь-яке читання-зміна-запис має кешувати об'єкт у регістрах. volatileне гарантує атомність.
занадто чесний для цього сайту

1
@Olaf: Завантаження чого-небудь в реєстр - це не те саме, що кешування. Кешування може впливати на кількість вантажів або магазинів або їх терміни.
supercat

28

Моє просте пояснення:

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

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


19

Граничне використання для летючих є наступним. Скажімо, ви хочете обчислити числову похідну функції 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?
Сергій Жуков

2
Різниця між hі hhполягає в тому hh, що операція прирізана до деякої негативної сили двох x + h - x. У цьому випадку x + hhі xрізняться точно за hh. Ви також можете взяти свою формулу, вона дасть той самий результат, оскільки x + hі x + hhє рівними (тут важливий знаменник).
Олександр К.

3
Чи не є читабельнішим способом написати це x1=x+h; d = (f(x1)-f(x))/(x1-x)? без використання летючого.
Сергій Жуков

Будь-яка посилання, що компілятор може викреслити другий рядок функції?
CoffeeTableEspresso

@CoffeeTableEspresso: Ні, вибач. Чим більше я знаю про плаваючу крапку, тим більше вважаю, що компілятору дозволено оптимізувати її лише у тому випадку, якщо явно сказано так, з -ffast-mathеквівалентом.
Олександр К.

11

Є два варіанти використання. Вони спеціально використовуються частіше при вбудованій розробці.

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

  2. Летючий використовується для доступу до точних місць пам’яті в оперативній пам’яті, ПЗУ і т. Д.… Це використовується частіше для управління пристроями, нанесеними на карту пам’яті, доступом до регістрів процесора та пошуку конкретних місць пам’яті.

Див. Приклади зі списком складання. Re: Використання C "летючого" ключового слова у вбудованому розвитку


"Компілятор не оптимізує функції, що використовують змінні, визначені мінливим ключовим словом" - це очевидно неправильно.
занадто чесний для цього сайту

10

Летючий також корисний, коли ви хочете змусити компілятор не оптимізувати певну кодову послідовність (наприклад, для написання мікро-еталону).


10

Я згадаю ще один сценарій, коли леткі речовини важливі.

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

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

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

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


7

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


Жоден компілятор не вважає мінливим значення "фізична адреса в оперативній пам'яті" або "обхід кешу".
curiousguy


5

На мою думку, не слід чекати занадто багато від 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за все гарантується тільки в скомпільований код з допомогою компілятора. Під час роботи процесор все ще можливо переробляє порядок передачі даних та команд щодо архітектури процесора. Обладнання могло отримати неправильні дані (припустимо, гаджет відображається на апаратному вводу-виводу). Бар'єр пам'яті необхідний між даними та призначенням команд.


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

5

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

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

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


5

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


1
Чи є у цій відповіді щось нове, про що раніше не говорилося?
slfan

3

Летучий елемент може бути змінений поза компільованим кодом (наприклад, програма може зіставити змінну змінну в регістр, нанесений на пам'ять.) Компілятор не застосовуватиме певні оптимізації до коду, який обробляє мінливу змінну - наприклад, вона виграла ' t завантажуйте його в регістр, не записуючи його в пам'ять. Це важливо при роботі з апаратними регістрами.


0

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

Найкраща перевага, яка спадає на думку, і яку варто згадати, прочитавши про непостійну, - запобігання відкоту змінної у випадку a longjmp. Немісцевий стрибок.

Що це означає?

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

Оскільки це питання не виходить за межі цього питання, я не збираюся setjmp/longjmpтут деталізувати , але про це варто прочитати; і як можна використовувати функцію мінливості для збереження останнього значення.


-2

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

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