Я заметил, что gcc12 не оптимизирует эти две функции для одного и того же кода (с -O3):
int x = 0;
void f(bool a)
{
if (a) {
++x;
}
}
void f2(bool a)
{
x += a;
}
В основном никаких трансформаций не происходит. Это можно увидеть здесь: https://godbolt.org/z/1G3n4fxEK
Оптимизация f для кода в f2 кажется тривиальной, и переход больше не потребуется. Однако мне любопытно, есть ли причина, по которой gcc этого не делает? Это как-то все еще медленнее или что-то еще? Я бы предположил, что это никогда не бывает медленнее, а иногда и быстрее, но я могу ошибаться.
Спасибо
@technosaurus Какова цель префикса !!, поскольку a уже является bool? Даже если бы это было не так, не было бы (bool)(condition) семантически эквивалентным и, возможно, более быстрым (в отладке) или, по крайней мере, таким же быстрым (в выпуске)? Я никогда не понимал, почему люди используют это обозначение вместо этого.
@ Жером Ричард, typedef для bool может быть int в коде до c99, поэтому можно было передать любое целочисленное значение в параметре bool - добавление этого числа может дать другой результат, чем добавление параметра !!... приведение к bool будет таким же как приведение к int в этих системах





Такая замена была бы неправильной в сценарии, когда один поток вызывает f(1), а другой поток вызывает f(0). Если к x фактически никогда не обращаются за пределами первого потока, в написанном коде не будет состояния гонки, но подстановка создаст его. Если x изначально равно 1, ничто не помешает обработать код как:
Это приведет к тому, что x останется со значением 1, когда поток 2 только что записал значение 2. Хуже того, если функция была вызвана в таком контексте, как:
x = 1;
f(1);
if (x != 1)
launch_nuclear_missiles_if_x_is_1_and_otherwise_make_coffee();
компилятор может распознать, что x всегда будет равно 2 после возврата из f(1), и, таким образом, сделать вызов функции безусловным.
Безусловно, такая замена редко вызывает проблемы в реальных ситуациях, но Стандарт явно запрещает преобразования, которые могут создать условия гонки, которых не было бы в исходном коде в том виде, в котором он написан.
Отличный ответ. Знаете ли вы способ указать компиляторам, что они могут делать такие оптимизации? Мое текущее решение до сих пор состояло в том, чтобы скопировать значение во временную переменную, выполнить операцию локально, а затем записать результат (что делает оптимизацию возможной, поскольку значение записывается обратно во всех случаях, поэтому ожидается, что потоки не будут изменить значение, так как это UB). Это решение не всегда заставляет компиляторы выполнять оптимизацию, и основная проблема заключается в том, что это очень обременительно для большого кода (это также делает код менее читаемым).
@JérômeRichard: К сожалению, полезный прогресс в направлении таких вещей был фактически подорван примерно в 2005 году, когда укоренилась идея о том, что вместо добавления конструкций для запуска таких преобразований было бы лучше использовать тот факт, что Стандарт не налагает требований на то, как реализации ведут себя в ситуациях. классифицируется как «неопределенное поведение», а любые программы, несовместимые с таким лечением, рассматриваются как «сломанные».
@JérômeRichard: В то время даже я думал, что такая концепция может быть интересна в качестве теоретического упражнения, но, к сожалению, она, похоже, почти полностью вытеснила исследования подходов, которые было бы проще реализовать, эффективнее и безопаснее.
@JérômeRichard: Что делает подход clang/gcc к оптимизации особенно грустным, так это то, что написание кода, использующего только стандартно определенное поведение, часто приводит к коду, который медленнее, чем тот, который clang и gcc, вероятно, создали бы из исходного кода, использующего специфичные для платформы поведение, над которым Стандарт отказывается от юрисдикции, и такой код можно было бы сделать надежным, если бы clang и gcc были немного менее агрессивными.
-fallow-store-data-races кажется связанным, но не влияет на генерацию кода для этой программы.
@MarcGlisse: такие переключатели намного страшнее, чем кажутся, поскольку в оптимизаторе нет согласованной модели абстракции. Есть много ситуаций, когда каждое из двух оптимизирующих преобразований было бы полезно, если бы применялось по отдельности, но при их сочетании приводили бы к катастрофическим результатам. Я скорее сомневаюсь, что люди, ответственные за преобразования, связанные с этим переключателем, систематически исследовали все другие преобразования, с которыми он мог взаимодействовать, и защищались от всех таких взаимодействий.
Я надеялся, что компилятор изменит f2 на f. Чтение и запись в память могут потребовать медленных транзакций для получения копии ячейки памяти и обновления других контроллеров шины о состоянии этой ячейки (Неверный -> Общий -> Модифицированный). Прыгать по обновлению на основе значения регистра довольно дешево; особенно с эффективностью предсказателей ветвления.
Гораздо более простая причина, по которой эта оптимизация не выполняется, заключается в том, что bool — это не что иное, как псевдоним для int. В частности, ничто не мешает вам передать произвольное целое число в вашу функцию:
int v = 5;
f2(*(bool *)&v);
// x = 5 here
Оказывается, GCC не считает bool просто псевдонимом int, поскольку сгенерированный код изменяется с int в качестве параметра. Кроме того, GCC по-прежнему выполняет оптимизацию с помощью x += (a != false) ? 1 : 0; (см. здесь), поэтому я думаю, что ответ supercat пока является лучшим объяснением. Действительно, все проверенные компиляторы (GCC, Clang, ICC) перестают генерировать версию без веток, когда хотя бы одна ветка не хранит x.
x += (a != false) ? 1 : 0 Конечно, это работает, gcc действительно может проверить значение логического значения.
bool — это псевдоним для _Bool. И bool также станет ключевым словом согласно C23. _Bool и int несовместимые типы. *(bool *)&v — это строгое нарушение алиасинга и неопределенное поведение.
Вот небольшой забавный пример godbolt.org/z/fqzbMq14r
The value is actually 5 -- Не знаю, какой урок вы пытались мне преподать, но не получилось. Значение этого «логического значения» действительно равно 5. Также позвольте мне сказать вам, насколько актуально сегодня то, что C23 получит правильный логический тип:
Многие компиляторы хорошо оптимизируют с помощью
x += !!(condition);!! Оптимизируется