Является ли оператор `if` избыточным перед операциями по модулю и перед операциями присваивания?

Рассмотрим следующий код:

unsigned idx;
//.. some work with idx
if( idx >= idx_max )
idx %= idx_max;

Можно упростить только до второй строки:

idx %= idx_max;

и достигнет того же результата.


Несколько раз я встречал следующий код:

unsigned x;
//... some work with x
if( x!=0 )
x=0;

Можно упростить до

x=0;

Вопросы:

  • Есть ли смысл использовать if и почему? Особенно с набором инструкций ARM Thumb.
  • Могут ли эти ifбыть опущены?
  • Какую оптимизацию делает компилятор?

46

Решение

Если вы хотите понять, что делает компилятор, вам нужно просто загрузить некоторую сборку. Я рекомендую этот сайт (я уже ввел код из вопроса)): https://godbolt.org/g/FwZZOb.

Первый пример более интересный.

int div(unsigned int num, unsigned int num2) {
if( num >= num2 ) return num % num2;
return num;
}

int div2(unsigned int num, unsigned int num2) {
return num % num2;
}

Формирует:

div(unsigned int, unsigned int):          # @div(unsigned int, unsigned int)
mov     eax, edi
cmp     eax, esi
jb      .LBB0_2
xor     edx, edx
div     esi
mov     eax, edx
.LBB0_2:
ret

div2(unsigned int, unsigned int):         # @div2(unsigned int, unsigned int)
xor     edx, edx
mov     eax, edi
div     esi
mov     eax, edx
ret

В основном, компилятор не оптимизировать ветку, по очень конкретным и логичным причинам. Если бы целочисленное деление стоило примерно столько же, сколько сравнение, тогда ветвление было бы довольно бессмысленным. Но целочисленное деление (модуль которого выполняется вместе, как правило,) на самом деле очень дорого: http://www.agner.org/optimize/instruction_tables.pdf. Числа сильно различаются в зависимости от архитектуры и целочисленного размера, но обычно это может быть задержка от 15 до 100 циклов.

Взяв ветку перед выполнением модуля, вы на самом деле можете сэкономить много работы. Однако обратите внимание: компилятор также не преобразует код без ветвления в ветвь на уровне сборки. Это связано с тем, что у ветви тоже есть недостаток: если модуль все равно оказывается необходимым, вы просто потратили немного времени.

Там нет никакого способа сделать разумное определение о правильной оптимизации, не зная относительную частоту, с которой idx < idx_max будет правдой. Таким образом, компиляторы (gcc и clang делают одно и то же) предпочитают отображать код относительно прозрачным способом, оставляя этот выбор на усмотрение разработчика.

Так что эта ветка могла бы быть очень разумным выбором.

Вторая ветвь должна быть совершенно бессмысленной, потому что сравнение и назначение являются сопоставимой стоимости. Тем не менее, вы можете увидеть в ссылке, что компиляторы все равно не будут выполнять эту оптимизацию, если у них есть ссылка на переменную. Если значение является локальной переменной (как в продемонстрированном вами коде), компилятор оптимизирует ветвь.

В общем, первый кусок кода, возможно, является разумной оптимизацией, второй, возможно, просто уставшим программистом.

66

Другие решения

Существует ряд ситуаций, когда запись переменной со значением, которое она уже содержит, может быть медленнее, чем ее чтение, обнаружение уже содержит желаемое значение и пропуск записи. Некоторые системы имеют кэш-память процессора, которая немедленно отправляет все запросы на запись в память. Хотя такие конструкции сегодня не являются обычным явлением, раньше они были довольно распространенными, поскольку они могут предложить значительную долю прироста производительности, которую может предложить полное кэширование чтения / записи, но за небольшую долю стоимости.

Код, подобный приведенному выше, также может быть актуален в некоторых ситуациях с несколькими процессорами. Чаще всего такая ситуация возникает, когда код, выполняющийся одновременно на двух или более ядрах ЦП, будет повторно попадать в переменную. В многоядерной системе кеширования с сильной моделью памяти ядро, которое хочет записать переменную, должно сначала договориться с другими ядрами, чтобы получить исключительное право владения строкой кеша, содержащей ее, и затем должно снова договориться, чтобы в следующий раз отказаться от такого контроля любое другое ядро ​​хочет прочитать или написать это. Такие операции склонны быть очень дорогими, и затраты придется нести, даже если каждая запись просто хранит значение уже сохраненного хранилища. Если местоположение становится равным нулю и никогда не записывается снова, однако, оба ядра могут одновременно удерживать строку кэша для неисключительного доступа только для чтения и никогда не должны договариваться об этом дальше.

Почти во всех ситуациях, когда несколько процессоров могут поражать переменную, переменная должна быть как минимум объявлена volatile, Единственное исключение, которое может быть применимо здесь, будет в случаях, когда все записи в переменную происходят после начала main() будет хранить то же значение, и код будет вести себя правильно, независимо от того, было ли какое-либо хранилище одним ЦП видимым в другом. Если выполнение некоторой операции несколько раз было бы бесполезным, но в остальном безвредным, и цель переменной — сказать, нужно ли это делать, тогда многие реализации смогут генерировать лучший код без volatile квалификатор, чем с, при условии, что они не пытаются повысить эффективность, делая запись безусловной.

Кстати, если бы к объекту обращались через указатель, был бы другой
Возможная причина для приведенного выше кода: если функция предназначена для принятия либо
const объект, где определенное поле равно нулю, или неconst объект, который
должно иметь это поле равным нулю, код, подобный приведенному выше, может быть необходим для
обеспечить определенное поведение в обоих случаях.

7

Относится к первому блоку кода: это микрооптимизация, основанная на рекомендациях Чендлера Каррута для Clang (см. Вот для получения дополнительной информации), однако это не обязательно означает, что это будет действительная микрооптимизация в этой форме (с использованием if, а не троичной) или на любом данном компиляторе.

Modulo — это достаточно дорогая операция, если код выполняется часто и существует сильное статистическое отклонение в одну или другую сторону от условного, предсказание ветвления ЦП (с учетом современного ЦП) значительно снизит стоимость инструкции ветвления ,

2

Мне кажется плохой идеей использовать «если есть».

Вы правы. Так или иначе idx >= idx_max, это будет под idx_max после idx %= idx_max, Если idx < idx_max, он останется неизменным независимо от того, соблюдается ли if или нет.

Хотя вы можете подумать, что ветвление по модулю может сэкономить время, реальный виновник, я бы сказал, заключается в том, что когда следуют ветки, конвейеры современных ЦП должны сбрасывать свой конвейер, а это стоит относительно много времени. Лучше не следовать ветке, чем целочисленное по модулю, которое стоит примерно столько же времени, сколько целочисленное деление.

РЕДАКТИРОВАТЬ: Оказывается, что модуль довольно медленно по сравнению с ветвью, как предлагают другие здесь. Вот парень, исследующий этот же вопрос: CppCon 2015: Чендлер Каррут «Настройка C ++: тесты, процессоры и компиляторы! О, Боже!» (предлагается в другом вопросе SO, связанном с другим ответом на этот вопрос).

Этот парень пишет компиляторы и думал, что это будет быстрее без ветки; но его тесты доказали его неправоту. Даже когда ветвление заняло только 20% времени, оно тестировалось быстрее.

Еще одна причина, по которой не стоит использовать if: на одну строку кода меньше, а кому-то еще нужно разобраться, что это значит. Парень в приведенной выше ссылке на самом деле создал макрос «более быстрый модуль». ИМХО, эта или встроенная функция — это путь для приложений, критичных к производительности, потому что ваш код будет гораздо более понятным без перехода, но будет выполняться так же быстро.

Наконец, парень из вышеупомянутого видео планирует сделать эту оптимизацию известной авторам компиляторов. Таким образом, if, вероятно, будет добавлен для вас, если не в коде. Следовательно, только мод сделает только, когда это произойдет.

1
По вопросам рекламы [email protected]