Чому компілятори C ++ не оптимізують це умовне булеве призначення як безумовне завдання?


117

Розглянемо наступну функцію:

void func(bool& flag)
{
    if(!flag) flag=true;
}

Мені здається, що якщо прапор має дійсне булеве значення, це було б рівнозначно беззастережному встановленню його true, як-от так:

void func(bool& flag)
{
    flag=true;
}

Але ні gcc, ні clang не оптимізують його таким чином - обидва генерують наступне на -O3рівні оптимізації:

_Z4funcRb:
.LFB0:
    .cfi_startproc
    cmp BYTE PTR [rdi], 0
    jne .L1
    mov BYTE PTR [rdi], 1
.L1:
    rep ret

Моє запитання: це просто те, що код є надто особливим випадком для оптимізації, чи є якісь вагомі причини, чому така оптимізація була б небажаною, враховуючи, що flagце не посилання volatile? Здається, єдиною причиною може бути те, що flagякимось чином можна було б мати trueнеозначувану falseцінність без невизначеної поведінки в момент її читання, але я не впевнений, чи можливо це.


8
Чи є у вас докази того, що це "оптимізація"?
Девід Шварц

1
@ 200_success Я не думаю, що ставити рядок коду з неробочою розміткою як заголовок - це добре. Якщо ви хочете більш конкретний заголовок, добре, але вибирайте англійське речення і намагайтеся уникати коду в ньому (наприклад, чому компілятори не оптимізують умовні записи до безумовних записів, коли вони можуть довести їх як еквівалентні? Або подібні). Крім того, що задні посилання не відображаються, не використовуйте їх у заголовку, навіть якщо ви використовуєте код.
Бакуріу

2
@Ruslan, хоча це оптимізація для самої функції, мабуть, не робить, коли він може вбудовувати код, але, здається, це робиться для вбудованої версії. Часто тільки внаслідок цього 1використовується константа часу компіляції . godbolt.org/g/swe0tc
Еван Теран

Відповіді:


102

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


EDIT

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

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

const bool foo = true;

int main()
{
    func(const_cast<bool&>(foo));
}

При безумовному func()записі це, безумовно, запускає невизначену поведінку (запис у пам'ять лише для читання припиняє програму, навіть якщо ефект запису інакше був би неоперативним).


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

3
На визначеність поведінки @Yakk цільова платформа не впливає. Сказати, що вона припинить програму, невірно, але сама УБ може мати далекосяжні наслідки, включаючи назальних демонів.
Джон Дворак

16
@Yakk Це залежить від того, що означає "пам'ять лише для читання". Ні, це не в чіпі ROM, але це дуже часто в розділі, завантаженому на сторінку, на якій немає доступу для запису, і ви отримаєте, наприклад, сигнал SIGSEGV або виняток STATUS_ACCESS_VIOLATION, коли спробуєте записати на нього.
Випадково832

5
"це, безумовно, викликає невизначеність поведінки". Ні. Невизначена поведінка є властивістю абстрактної машини. Саме те, що говорить код, визначає, чи є UB. Компілятори не можуть його викликати (хоча якщо помилка компілятора може спричинити неправильну поведінку програм).
Ерік М Шмідт

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

48

Крім відповіді Леона про ефективність:

Припустимо, flagє true. Припустимо, дві нитки постійно дзвонять func(flag). Функція, як написано, у цьому випадку нічого не зберігає flag, тому це має бути безпечно для потоків. Два потоки мають доступ до тієї самої пам'яті, але лише для її читання. Беззастережно установки flagз trueдопомогою двох різних потоків буде написання однієї і тієї ж пам'яті. Це не безпечно, це небезпечно, навіть якщо записані дані ідентичні тим, які вже є.


9
Я думаю, що це результат застосування [intro.races]/21.
Griwes

10
Дуже цікаво. Тому я читаю це як: Компілятору ніколи не дозволяється «оптимізувати» операцію запису, де абстрактна машина не мала б її.
Мартін Ба

3
@MartinBa Здебільшого так. Але якщо компілятор може довести, що це не має значення, наприклад, тому що він може довести, що жоден інший потік не міг би мати доступ до цієї певної змінної, то це може бути добре.

13
Це небезпечно лише в тому випадку, якщо система, на яку націлений компілятор, робить її небезпечною . Я ніколи не розроблявся в системі, де запис 0x01у байт, що вже 0x01викликає "небезпечну" поведінку. У системі із словом або словом пам'яті доступ до неї буде; але оптимізатор повинен знати про це. На сучасному ПК або телефоні ОС жодних проблем не виникає. Тож це не вагома причина.
Якк - Адам Невраумон

4
@Yakk Насправді, думаючи навіть більше, я вважаю, що це все-таки правильно, навіть для звичайних процесорів. Я думаю, ти маєш рацію, коли процесор може записати в пам'ять безпосередньо, але припустимо, flagце на сторінці копіювання під час запису. Тепер на рівні процесора може бути визначена поведінка (помилка сторінки, нехай ОС це обробляє), але на рівні ОС вона все ще може бути невизначена, правда?

13

Я не впевнений у поведінці C ++ тут, але в C пам'ять може змінитися, тому що якщо пам'ять містить ненульове значення, відмінне від 1, воно залишатиметься незмінним при перевірці, але змінюється на 1 з чеком.

Але оскільки я не дуже добре володію C ++, я не знаю, чи така ситуація навіть можлива.


Чи все-таки це було б правдою _Bool?
Руслан

5
У C, якщо в пам'яті є значення, яке, за словами ABI, не вірно для свого типу, то це представлення пастки, і читання представлення пастки - це не визначене поведінка. У C ++ це може статися лише під час читання неініціалізованого об'єкта, і це читання неініціалізованого об'єкта, який є UB. Але якщо ви можете знайти ABI, який говорить, що будь-яке ненульове значення є дійсним для типу bool/ _Boolі означає true, то в цьому конкретному ABI ви, мабуть, маєте рацію.

1
@Ruslan З компіляторами, які використовують Itanium ABI, і на процесорах ARM, C _Boolі C ++ boolє або одного типу, або сумісними типами, які відповідають тим же правилам. У MSVC вони однакового розміру та вирівнювання, але немає офіційної заяви про те, чи вони використовують однакові правила.
Час Джастіна - Відновіть Моніку

1
@JustinTime: C <stdbool.h>включає " typedef _Bool bool; А" так, на x86 (принаймні, в системі V ABI), bool/ _Boolповинні бути 0 або 1, при цьому верхні біти байта очищені. Я не вважаю це пояснення правдоподібним.
Пітер Кордес

1
@JustinTime: Це правда, я мав би лише зазначити, що в усіх, безумовно, є однакова семантика у всіх ароматах x86 системи V ABI, про що йшлося в цьому питанні. (Я можу сказати, оскільки перший аргумент funcбув переданий в RDI, тоді як Windows використовує RDX).
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.