Чи дозволено компілятору оптимізувати локальну мінливу змінну?


79

Чи дозволено компілятору оптимізувати це (згідно зі стандартом C ++ 17):

int fn() {
    volatile int x = 0;
    return x;
}

до цього?

int fn() {
    return 0;
}

Якщо так, чому? Якщо ні, чому ні?


Ось деякі роздуми з цього приводу: поточні компілятори компілюють fn()як локальну змінну, яку поміщають у стек, а потім повертають її. Наприклад, на x86-64, gcc створює це:

mov    DWORD PTR [rsp-0x4],0x0 // this is x
mov    eax,DWORD PTR [rsp-0x4] // eax is the return register
ret    

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

mov    edx,0x0 // this is x
mov    eax,edx // eax is the return
ret    

Тут edxмагазини x. Але тепер, чому тут зупинятися? Як edxі eaxдорівнюють нулю, ми могли б просто сказати:

xor    eax,eax // eax is the return, and x as well
ret    

І ми перейшли fn()на оптимізовану версію. Чи є це перетворення дійсним? Якщо ні, який крок недійсний?


1
Коментарі не призначені для розширеного обговорення; цю розмову переміщено до чату .


@philipxy: Йдеться не про те, "що може дати". Мова йде про те, чи дозволено перетворення. Тому що, якщо це не дозволено, то воно не повинно створювати трансформовану версію.
geza

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

1
@philipxy: Я уточню своє запитання, що мова йде про стандарт. Зазвичай це мається на увазі під такими запитаннями. Мене цікавить, що говорить стандарт.
geza

Відповіді:


63

Ні. Доступ до volatileоб’єктів вважається спостережуваною поведінкою, точно так само, як введення / виведення, без особливої ​​різниці між місцевими жителями та глобальними.

Найменшими вимогами до відповідного впровадження є:

  • Доступ до volatileоб'єктів оцінюється строго за правилами абстрактної машини.

[...]

Вони в сукупності називаються спостережуваною поведінкою програми.

N3690, [вступ.виконання], №8

Те , як саме це можна спостерігати, виходить за рамки стандарту і потрапляє прямо на територію конкретної реалізації, точно так само, як введення / виведення та доступ до глобальних volatileоб'єктів. volatileозначає "ви думаєте, що знаєте все, що тут відбувається, але це не так; довіряйте мені і робіть це, не надто розумно, бо я у вашій програмі роблю свої секретні речі з вашими байтами". Це насправді пояснюється в [dcl.type.cv] №7:

[Примітка: volatileце натяк на реалізацію, щоб уникнути агресивної оптимізації за участю об’єкта, оскільки значення об’єкта може бути змінено засобами, не виявленими реалізацією. Крім того, для деяких реалізацій volatile може вказувати на те, що для доступу до об'єкта потрібні спеціальні апаратні інструкції. Детальну семантику див. У 1.9. Загалом, семантика volatile має бути такою ж на C ++, як і на C. - кінцева примітка]


2
Оскільки це питання з найбільшим голосом, і питання було розширено редагуванням, було б непогано відредагувати цю відповідь, щоб обговорити нові приклади оптимізації.
Хайд

Правильним є "так". Ця відповідь чітко не відрізняє абстрактні машинні спостережувані від генерованого коду. Останнє визначається реалізацією. Наприклад, можливо, для використання з даним налагоджувачем мінливий об'єкт гарантовано знаходиться в пам'яті та / або реєструється; наприклад, як правило, у відповідній цільовій архітектурі гарантуються записи та / або читання для нестабільних об'єктів у визначених прагмою спеціальних місцях пам'яті. Реалізація визначає, як доступ відображається в коді; він вирішує, як і коли об'єкт (и) "можуть бути змінені засобами, які неможливо виявити реалізацією". (Див. Мої коментарі до запитання.)
philipxy

12

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

for (unsigned i = 0; i < n; ++i) { bool looped = true; }

Цей не може:

for (unsigned i = 0; i < n; ++i) { volatile bool looped = true; }

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

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

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


Отже, ви кажете, що остаточна xor ax, ax(де axвважається volatile x) версія у питанні є дійсною чи недійсною? IOW, яка ваша відповідь на запитання?
Хайд,

@hyde: Питання, коли я його читав, було: "чи можна усунути змінну", і моя відповідь - "Ні". Що стосується конкретної реалізації x86, яка піднімає питання про те, чи можна мінливий вмістити в реєстрі, я не зовсім впевнений. Навіть якщо його звести до xor ax, ax, однак, цей код коду не може бути усунутий, навіть якщо він виглядає марним, і не може бути об'єднаний. У моєму прикладі циклу скомпільований код повинен був би виконуватися xor ax, axn разів, щоб задовольнити спостережуване правило поведінки. Сподіваємось, редакція відповідає на ваше запитання.
Річі

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

2
@hyde: Насправді, я використовую летючі речовини таким чином у тестах, щоб уникнути того, щоб компілятор оптимізував цикл, який в іншому випадку нічого не робить. Тож я дуже сподіваюся, що я маю рацію щодо цього: =)
rici

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

10

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

Якщо у вас є цей код:

{
    volatile int x;
    x = 0;
}

Я вважаю, що компілятор може оптимізувати його за правилом як би , припускаючи, що:

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

  2. Компілятор не надає вам механізму зовнішнього доступу до нього volatile

Обґрунтування полягає лише в тому, що ви все одно не могли спостерігати різницю через критерій No2.

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

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

{
    volatile int x = 0;
}

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


6

Теоретично обробник переривань міг

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

... таким чином, fn()повертаючи ненульове значення.


1
Або ви можете легше зробити це за допомогою налагоджувача, встановивши точку зупинки в fn(). Використання volatileгенерує код-ген, подібний gcc -O0до цієї змінної: розлив / перезавантаження між кожним оператором C. ( -O0все ще може поєднувати кілька доступів в одному висловлюванні, не порушуючи узгодженість налагоджувача, але volatileце заборонено робити.)
Пітер Кордес,

Або простіше, використовуючи налагоджувач :) Але, який стандарт говорить, що змінну потрібно спостерігати? Я маю на увазі, реалізація може вибрати, що вона повинна спостерігатися. Інший може сказати, що це не спостерігається. Чи останній порушує стандарт? Можливо ні. Стандарт не визначає, як взагалі можна спостерігати локальну летючу змінну.
geza

Навіть, що це означає "спостережуваний"? Чи слід його розміщувати на стосі? Що робити, якщо реєстр зберігається x? Що робити, якщо на x86-64 xor rax, raxутримує нуль (я маю на увазі, реєстр повернених значень x), що, звичайно, може легко спостерігатися / змінюватися налагоджувачем (тобто інформація про символи налагодження xзберігається в rax). Чи це порушує стандарт?
geza

2
−1 Будь-який дзвінок fn()можна вбудувати. З MSVC 2017 та режимом випуску за замовчуванням це так. Тоді не існує "всередині fn()функції". Незважаючи на це, оскільки змінна є автоматичним зберіганням, немає “передбачуваного зсуву”.
Вітаю і hth. - Альф

1
0 @berendi: Так, ти маєш рацію щодо цього, і я помилився в цьому. Вибачте, поганий ранок для мене в цьому відношенні (двічі неправильно). Тим не менше, IMO не має сенсу сперечатися, як компілятор може підтримувати доступ через інше програмне забезпечення, оскільки він може це робити незалежно від того volatile, і тому, volatileщо не змушує його надавати цю підтримку. І тому я прибираю голос проти (я помилявся), але не голосую, бо вважаю, що цей напрямок міркувань не з’ясовується.
Вітаю і hth. - Альф

6

Я просто збираюся додати детальне посилання на правило як би і на нестабільне ключове слово. (Внизу цих сторінок дотримуйтесь "див. Також" та "Посилання", щоб прослідкувати до вихідних специфікацій, але я вважаю, що cppreference.com набагато легше читати / розуміти.)

Зокрема, я хочу, щоб ви прочитали цей розділ

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

Тож ключове слово volatile конкретно стосується вимкнення оптимізації компілятора на glvalues . Єдине, на що тут може вплинути нестабільне ключове слово, можливо return x, компілятор може робити все, що хоче, з рештою функції.

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

Примітка: У цих випадках завжди припускайте, що компілятор буде робити навпаки того, що ви хотіли / потребували. Вам слід або відключити оптимізацію (принаймні для цього модуля), або спробувати знайти більш визначену поведінку для того, що ви хочете. (Ось чому одиничне тестування так важливе.) Якщо ви вважаєте, що це дефект, вам слід повідомити про це у розробників C ++.


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

glvalue Вираз glvalue - це або lvalue, або xvalue.

Властивості:

Значення glvalue може неявно перетворюватися на перше значення зі значенням lvalue-to-rvalue, array-to-pointer або функцією-показником неявного перетворення. Значення glvalue може бути поліморфним: динамічний тип об'єкта, який він ідентифікує, не обов'язково є статичним типом виразу. Значення glvalue може мати неповний тип, якщо це дозволено виразом.


xvalue Наступні вирази є виразами xvalue:

виклик функції або перевантажений вираз оператора, типом повернення якого є посилання rvalue на об'єкт, наприклад std :: move (x); a [n], вбудований вираз індексу, де один операнд є значенням масиву r; am, член виразу об'єкта, де a - значення r, а m - нестатичний член даних нереференційного типу; a. * mp, вказівник на член вираження об'єкта, де a - значення r, а mp - вказівник на елемент даних; а? b: c, трійковий умовний вираз для деяких b та c (докладніше див. визначення); вираз вибору для посилання rvalue на тип об'єкта, наприклад static_cast (x); будь-який вираз, який позначає тимчасовий об'єкт після тимчасової матеріалізації. (з C ++ 17) Властивості:

Те саме, що rvalue (внизу). Те саме, що і glvalue (внизу). Зокрема, як і всі rvalues, xvalues ​​прив'язуються до посилань rvalue, і як і всі glvalues, xvalues ​​можуть бути поліморфними, а x-значення некласу можуть бути cv-кваліфікованими.


lvalue Наступні вирази є виразами lvalue:

ім'я змінної, функції або члена даних, незалежно від типу, наприклад std :: cin або std :: endl. Навіть якщо типом змінної є посилання rvalue, вираз, що складається з її імені, є виразом lvalue; виклик функції або перевантажений вираз оператора, типом повернення якого є посилання lvalue, наприклад std :: getline (std :: cin, str), std :: cout << 1, str1 = str2 або ++ it; a = b, a + = b, a% = b та всі інші вбудовані вирази присвоєння та складеного присвоєння; ++ a та --a, вбудовані вирази попереднього збільшення та попереднього зменшення; * p, вбудований непрямий вираз; a [n] та p [n], вбудовані вирази нижчого індексу, за винятком випадків, коли a є значенням масиву r (починаючи з C ++ 11); am, член виразу об'єкта, за винятком випадків, коли m є перелічувачем членів або нестатичною функцією члена, або де a - значення r, а m - нестатичний член даних нереференсного типу; p-> m, вбудований член виразу покажчика, за винятком випадків, коли m є перечислювачем членів або нестатичною функцією члена; a. * mp, вказівник на член вираження об'єкта, де a - значення l, а mp - вказівник на елемент даних; p -> * mp, вбудований вказівник на член виразу вказівника, де mp - вказівник на елемент даних; a, b, вбудований вираз коми, де b - значення l; а? b: c, потрійний умовний вираз для деяких b і c (наприклад, коли обидва значення l є однотипними, але докладніше див. визначення); рядковий літерал, наприклад "Привіт, світе!"; вираз вибору для посилального типу lvalue, наприклад static_cast (x); виклик функції або перевантажений вираз оператора, тип повернення якого є посиланням rvalue на функцію; вираз вибору для посилання rvalue на тип функції, такий як static_cast (x). (з C ++ 11) Властивості:

Те саме, що і glvalue (внизу). Адреса значення може бути прийнята: & ++ i 1 та & std :: endl є дійсними виразами. Модифікується значення lvalue може використовуватися як лівий операнд вбудованих операторів присвоєння та складеного призначення. Lvalue може використовуватися для ініціалізації посилання lvalue; це пов'язує нову назву з об'єктом, ідентифікованим виразом.


як би правило

Компілятору С ++ дозволяється вносити будь-які зміни в програму, якщо виконується наступне:

1) У кожній точці послідовності значення всіх летких об'єктів стабільні (попередні оцінки завершені, нові оцінки не розпочаті) (до C ++ 11) 1) Доступ (читання та запис) до летких об'єктів відбувається суворо відповідно до семантики виразів, у яких вони трапляються. Зокрема, вони не впорядковуються щодо інших летких доступів на тій самій нитці. (починаючи з C ++ 11) 2) При завершенні програми дані, записані у файли, точно відповідають вимогам програми. 3) Підказковий текст, який надсилається на інтерактивні пристрої, буде показаний до того, як програма зачекає на введення. 4) Якщо підтримується прагма ISO C # pragma STDC FENV_ACCESS і встановлено значення ON,


Якщо ви хочете прочитати специфікації, я вважаю, що саме ці вам потрібно прочитати

Список літератури

Стандарт С11 (ISO / IEC 9899: 2011): 6.7.3 Кваліфікатори типу (с: 121-123)

Стандарт C99 (ISO / IEC 9899: 1999): 6.7.3 Кваліфікатори типу (p: 108-110)

Стандарт C89 / C90 (ISO / IEC 9899: 1990): 3.5.3 Кваліфікатори типу


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

1
@meneldal: Це занадто широке твердження. _AddressOfReturnAddressНаприклад, використання включає аналіз стека. Люди аналізують стек з поважних причин, і це не обов'язково тому, що сама функція покладається на нього для коректності.
user541686

1
glvalue тут:return x;
geza

@geza Вибачте, це все важко читати. Це glvalue, тому що x є змінною? Крім того, для "не може бути оптимізовано", чи означає це, що компілятор взагалі не може оптимізувати, або що він не може оптимізувати, змінивши вираз? (Це виглядає так, як тут компілятору все ще дозволено оптимізувати, оскільки їх немає порядку доступу для підтримки, а вираз все ще вирішується, лише більш оптимізованим способом). специфікації.
Тезра

Ось цитата з вашої власної відповіді :) "Наступні вирази є виразами lvalue: ім'я змінної ..."
geza,

-1

Я думаю, що я ніколи не бачив локальної змінної з використанням volatile, яка не була вказівником на volatile. Як і в:

int fn() {
    volatile int *x = (volatile int *)0xDEADBEEF;
    *x = 23;   // request data, 23 = temperature 
    return *x; // return temperature
}

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

Там набагато легше міркувати, чому оптимізація може змінити спостережувані ефекти. Але те саме правило застосовується до вашої локальної мінливої ​​змінної. Компілятор повинен поводитися так, ніби доступ до x спостерігається і не може оптимізувати його.


3
Але це не локальна летюча змінна, це локальний енергонезалежний вказівник на мінливий int за відомою адресою.
марний

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

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

Я злився, коли int fn (аргумент const volatile int) не компілювався.
Джошуа

4
Редагування робить вашу відповідь не неправильною, але вона просто не відповідає на питання. Це варіант використання підручника volatile, і він не має нічого спільного з тим, що він є місцевим. Це могло б так само бути static volatile int *const x = ...в глобальному масштабі, і все, що ви кажете, все одно було б незмінним. Це як додаткові базові знання, необхідні для розуміння питання, які, мабуть, є не у всіх, але це не справжня відповідь.
Пітер Кордес,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.