Почему компиляторы 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 Я не думаю, что размещение строки кода с нерабочей разметкой в ​​качестве заголовка - это хорошо. Если вам нужен более конкретный заголовок, хорошо, но выберите английское предложение и постарайтесь избегать кода в нем (например, почему бы компиляторам не оптимизировать условную запись на безусловную запись, когда они могут доказать, что они эквивалентны? Или аналогичные). Кроме того, поскольку обратные кавычки не отображаются, не используйте их в заголовке, даже если вы используете код.
Bakuriu
2
@Ruslan, хотя он, похоже, не выполняет эту оптимизацию для самой функции, когда он может встроить код, он, похоже, делает это для встроенной версии. Часто просто приводит к использованию постоянной времени компиляции 1. godbolt.org/g/swe0tc
Эван Теран

Ответы:

102

Это может отрицательно сказаться на производительности программы из-за соображений согласованности кеша . Запись в flagкаждый func()вызов будет загрязнять содержащуюся строку кэша. Это произойдет независимо от того, что записываемое значение точно соответствует битам, найденным в адресе назначения до записи.


РЕДАКТИРОВАТЬ

hvd предоставил еще одну вескую причину , препятствующую такой оптимизации. Это более веский аргумент против предложенной оптимизации, поскольку она может привести к неопределенному поведению, тогда как мой (исходный) ответ касался только аспектов производительности.

После небольшого размышления я могу предложить еще один пример, почему компиляторам следует строго запретить - если они не могут доказать, что преобразование безопасно для конкретного контекста - вводить безусловную запись. Рассмотрим этот код:

const bool foo = true;

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

При безусловной записи func()это определенно вызывает неопределенное поведение (запись в постоянную память завершит программу, даже если эффект записи в противном случае был бы безоперационным).

Леон
источник
7
Это также может положительно сказаться на производительности, поскольку вы избавились от ветки. Так что я не думаю, что этот конкретный случай имеет смысл обсуждать без очень конкретной системы.
Lundin
3
Целевая платформа не влияет на определение поведения @Yakk. Сказать, что он завершит программу, неверно, но сам UB может иметь далеко идущие последствия, в том числе носовые демоны.
Джон Дворжак
16
@Yakk Это зависит от того, что имеется в виду под «постоянной памятью». Нет, его нет в микросхеме ПЗУ, но он очень часто находится в разделе, загруженном на страницу, для которой не разрешен доступ для записи, и вы получите, например, сигнал SIGSEGV или исключение STATUS_ACCESS_VIOLATION при попытке записи в него.
Random832
5
"это определенно вызывает неопределенное поведение". Нет. Неопределенное поведение - это свойство абстрактной машины. То, что говорит код, определяет, присутствует ли UB. Компиляторы не могут вызвать это (хотя в случае ошибки компилятор может привести к неправильному поведению программ).
Эрик М. Шмидт
7
Это отказ от constпередачи в функцию, которая может изменять данные , являющиеся источником неопределенного поведения, а не безусловной записи. Доктор, мне больно, когда я делаю это ....
Спенсер
48

Помимо ответа Леона о производительности:

Предположим , что flagесть true. Предположим, что два потока постоянно звонят func(flag). В этом случае функция в том виде, в котором она написана, ничего не сохраняет flag, поэтому она должна быть потокобезопасной. Два потока обращаются к одной и той же памяти, но только для ее чтения. Безоговорочно установки flagс trueпомощью двух разных потоков будет написание одной и той же памяти. Это небезопасно, это небезопасно, даже если записываемые данные идентичны данным, которые уже есть.


источник
9
Я думаю , что это результат применения [intro.races]/21.
Гривес
10
Очень интересно. Я читаю это так: компилятору никогда не разрешается «оптимизировать» операцию записи, если бы у абстрактной машины ее не было.
Мартин Ба
3
@MartinBa В основном так. Но если компилятор может доказать, что это не имеет значения, например, потому что он может доказать, что никакой другой поток не может иметь доступ к этой конкретной переменной, тогда все в порядке.
13
Это небезопасно только в том случае, если система, на которую нацелен компилятор, делает его небезопасным . Я никогда не разрабатывал систему, в которой запись 0x01в уже существующий байт 0x01вызывает "небезопасное" поведение. В системе с доступом к памяти слова или двойного слова это было бы; но оптимизатор должен знать об этом. На современном ПК или ОС телефона проблем не возникает. Так что это не веская причина.
Якк - Адам Неврамонт
4
@Yakk На самом деле, если подумать, я думаю, что это все-таки правильно, даже для обычных процессоров. Я думаю, вы правы, когда ЦП может писать в память напрямую, но предположим, что он flagнаходится на странице копирования при записи. Теперь на уровне ЦП поведение может быть определено (ошибка страницы, пусть ОС обрабатывает это), но на уровне ОС оно все еще может быть неопределенным, верно?
13

Я не уверен в поведении C ++ здесь, но в C память может измениться, потому что, если память содержит ненулевое значение, отличное от 1, оно останется неизменным при проверке, но изменится на 1 при проверке.

Но поскольку я не очень хорошо владею C ++, я не знаю, возможна ли такая ситуация.

glglgl
источник
Будет ли это по-прежнему правдой _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 (по крайней мере, в ABI System V) bool/ _Boolдолжны быть либо 0, либо 1, с очищенными старшими битами байта. Я не думаю, что это объяснение правдоподобно.
Питер Кордес
1
@JustinTime: Это правда, я должен был просто указать, что он определенно имеет одинаковую семантику во всех разновидностях x86 ABI System V, о чем и был этот вопрос. (Я могу сказать, потому что первый аргумент funcбыл передан в RDI, а Windows будет использовать RDX).
Питер Кордес