Является ли выражение `if` излишним перед модулем и перед назначением операций? - программирование
Подтвердить что ты не робот

Является ли выражение `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 быть опущены?
  • Какая оптимизация компилятора?
4b9b3361

Ответ 1

Если вы хотите понять, что делает компилятор, вам нужно просто подтянуть сборку. Я рекомендую этот сайт (я уже ввел код с вопроса)): 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 делают то же самое) предпочитают сопоставлять код относительно прозрачным способом, оставляя этот выбор в руках разработчика.

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

Modulo - достаточно дорогостоящая операция, если код выполняется часто, и существует сильная статистическая нагрузка на одну сторону или другую из условного числа, предсказание ветвления CPU (учитывая современный процессор) значительно сократит стоимость инструкция ветвления.

Ответ 4

Кажется, плохая идея использовать, если есть, для меня.

Вы правы. Будь или нет idx >= idx_max, он будет находиться под idx_max после idx %= idx_max. Если idx < idx_max, он не изменится, будет ли следовать if или нет.

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

EDIT: Оказывается, что модуль довольно медленный по отношению к ветке, как это предлагают другие. Вот парень, рассматривающий этот тот же самый вопрос: CppCon 2015: Chandler Carruth "Настройка С++: тесты, процессоры и компиляторы! Oh My!" (предложенный в другом вопросе SO, связанный с другим ответом на этот вопрос).

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

Еще одна причина не иметь: если еще меньше строк кода для поддержки, а для кого-то другого, чтобы решить, что это значит. Парень в вышеупомянутой ссылке фактически создал макрос "более быстрый модуль". IMHO, эта или встроенная функция - это путь для критически важных приложений, потому что ваш код будет настолько понятнее без ветки, но будет выполняться так быстро.

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