Почему невыгодно встраивать функции с помощью циклов или операторов переключения?

Я заметил, что руководство по стилю C ++ от Google предостерегает от встраивания функций с помощью циклов. или операторы переключения:

Еще одно полезное эмпирическое правило: обычно неэффективно встраивать функции с помощью циклов или операторов переключения (если, в общем случае, оператор цикла или переключения никогда не выполняется).

Другие комментарии к StackOverflow подтвердили это мнение.

Почему функции с циклами или операторами switch (или gotos) не подходят для встраивания или не совместимы с ним. Применимо ли это к функциям, которые содержат любой тип прыжка? Применимо ли это к функциям с if операторами? Кроме того (и это может быть несколько не связано), почему не рекомендуется встраивать функции, возвращающие значение?

Меня особенно интересует этот вопрос, потому что я работаю с сегментом кода, чувствительного к производительности. Я заметил, что после встраивания функции, содержащей серию операторов if, производительность значительно снижается. Я использую GNU Make 3.81, если это уместно.


person olliezhu    schedule 24.03.2015    source источник
comment
Я бы рекомендовал оставить решения о встраивании компилятору - как и составители компиляторов, которые с радостью игнорируют, какие функции объявил программист inline.   -  person EOF    schedule 24.03.2015
comment
Я использую GNU Make 3.81, если это уместно. Более подходящей частью может быть используемая реализация компилятора C ++.   -  person πάντα ῥεῖ    schedule 24.03.2015
comment
Встроенный обычно находится в пределах 5 строк оптимизации кода, операторы цикла и переключения обычно имеют большое количество логики. А если вам нужно оптимизировать, правило 80/20, выяснить патогенез, тщательная оптимизация. Все после оптимизации с учетом inline   -  person Ron Tang    schedule 24.03.2015
comment
Обычно я не использую inline без проверки asm.   -  person user3528438    schedule 24.03.2015
comment
Я изучил значительную часть нашего кода в виде дампа инструкций в kdb, и по моему опыту, наш компилятор фактически встраивает функции, помеченные как встроенные, и явно вызывает функции, которые не являются встроенными. В моем случае я думаю, что могу предположить, что этот процесс относительно прост.   -  person olliezhu    schedule 24.03.2015
comment
@uhwuggawuh: опять же, какой компилятор, какая архитектура? Архитектура имеет значение: в арках с соглашениями о вызовах на основе стека (x86 stdcall) вызов функции имеет довольно высокую внутреннюю стоимость. В соглашениях о вызовах на основе регистров (x86-64, MIPS, x86 fastcall) встраивание в основном позволяет переупорядочивать инструкции в вызове функции и может устранить избыточные вычисления. Некоторые из этих преимуществ также можно получить за счет постоянного распространения.   -  person EOF    schedule 24.03.2015
comment
@EOF: часть кода работает на архитектуре x86-64, а часть кода является микропрограммой для архитектуры сетевого процессора (которая имеет соглашение о вызовах на основе регистров). Итак, в этом случае вы говорите, что когда я встраиваю функции, я не увижу значительных улучшений, связанных с устранением накладных расходов на вызов функций?   -  person olliezhu    schedule 24.03.2015
comment
@EOF Я согласен с вашим мнением, но не с вашими заявлениями, которые компиляторы игнорируют inline.   -  person Neil Kirk    schedule 24.03.2015


Ответы (4)


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

Если имеется несколько операторов ветвления, успешное предсказание ветвления экономит намного больше циклов, чем затраты на вызов функции.

Аналогичная логика применяется к развертыванию циклов с помощью операторов switch.


В упомянутом руководстве Google ничего не упоминается о функциях, возвращающих значения, поэтому я предполагаю, что ссылка находится в другом месте и требует другого вопроса с явной ссылкой.

person rici    schedule 24.03.2015
comment
Да, ссылка на функции, возвращающие значения, была тем, что я видел в StackOverflow по теме встроенных функций, часто одновременно с предупреждениями о встраивании функций с помощью циклов и операторов переключения. - person olliezhu; 24.03.2015
comment
@uhwuggawuh: Ссылка, которую вы дали для этого, - это 0-балльный ответ автора с 1 репутацией ... - person EOF; 24.03.2015
comment
@rici: Вы говорите, что причина того, что встраивание не является рентабельным в таких ситуациях, заключается в том, что узкое место производительности будет заключаться в относительно длительном цикле цикла или ветвления? - person olliezhu; 24.03.2015
comment
@uhwuggawuh: Нет. Я говорю, что отсутствие встраивания повысит вероятность того, что ЦП будет правильно предсказывать переходы, что значительно ускорит циклы и другие переходы. - person rici; 24.03.2015
comment
@rici: если вызов одной и той же функции в разных местах с очень разными аргументами не сбивает с толку предсказатель ветвления ... - person EOF; 24.03.2015
comment
@EOF: Это был не единственный случай, когда я это видел; есть несколько других тем в StackOverflow, которые это подтвердили. например stackoverflow.com/questions/60830 / - person olliezhu; 24.03.2015
comment
@uhwuggawuh: Цитата в SO, которую вы даете (помимо других ее недостатков), не говорит, что вы не должны встраивать функции, которые включают эти четыре функции. В нем говорится, что компилятор может отказаться от встраивания таких функций. Это верно только потому, что компилятор всегда может отказаться от встраивания функции, и он не обязан объяснять вам, почему. - person rici; 24.03.2015
comment
@EOF: Я только сказал, что более вероятно. - person rici; 24.03.2015
comment
@rici: Спасибо за разъяснения. Не могли бы вы также подробнее рассказать о том, как это применимо и к разворачиванию цикла? Вы говорите, что компилятору также сложнее разворачивать циклы для повышения производительности, если функция встроена? - person olliezhu; 24.03.2015
comment
@uhwuggawuh: Большинство предсказателей ветвления создают историю прошлых ветвей и используют виртуальный адрес ветки, чтобы найти ее историю. Если вы развернете цикл, содержащий switch, то вы дублируете все ветки в switch, заставляя ЦП дольше получать хорошую историю ветвлений, а также потребляя больше (ограниченное количество) слотов прогнозирования ветвлений ЦП. - person Brendan; 24.03.2015
comment
@uhwuggawuh: Также; имейте в виду, что (в общем, для всех советов по оптимизации) возможны угловые случаи, когда совет неверен. Простым примером может быть встраивание функции, содержащей switch, где большинство вызывающих абонентов используют постоянные аргументы (и где компилятор может использовать сворачивание констант для удаления ветвей, если оно было встроено). - person Brendan; 24.03.2015
comment
@Brendan: но в таком случае компилятор может быть более встроенным, независимо от того, запрашивается ли это или нет. - person rici; 24.03.2015

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

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

В случае операторов switch я могу только догадываться. Объяснение может заключаться в том, что таблицы переходов могут быть довольно большими, тратя гораздо больше памяти в сегменте кода, чем очевидно.

Я думаю, что ключевое слово здесь рентабельно. Функции, требующие большого количества циклов или памяти, обычно не стоит встраивать.

person nwellnhof    schedule 25.03.2015

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

Встраивание решений в компилятор имеет мало общего с «Делаем простой предсказатель переходов счастливым». Или менее запутанный.

Во-первых, целевой ЦП может даже не иметь предсказания ветвления.

Во-вторых, конкретный пример:

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

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

int f(int s)
{
 ...;
 switch (s) {
   case 1: ...; break;
   case 2: ...; break;
   case 42: ...; return ...;
 }
 return ...;
}

void g(...)
{
  int x=f(42);
  ...
}

Когда компилятор решает встроить f, он заменяет правую часть присваивания телом f. Он заменяет фактический параметр 42 на формальный параметр s и внезапно обнаруживает, что переключатель находится на постоянном значении ... поэтому он отбрасывает все другие ветви, и, надеюсь, известное значение позволит дальнейшие упрощения (т.е. они каскадируются).

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

Если вам не повезло, размер кода увеличивается, локальность во время выполнения уменьшается, а ваш код работает медленнее.

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

Дело в том, что чертовски сложно предсказать, что произойдет с фрагментом кода, даже если вы знаете все способы, которыми компилятор может его изменить. Я не могу вспомнить, кто это сказал, но нельзя распознать исполняемый код, созданный оптимизирующим компилятором.

person user1666959    schedule 25.03.2015
comment
Я бы хотел, чтобы был стандартный способ указания кода, что следует использовать определенный алгоритм, если он может быть встроен в определенные значения в качестве констант и другой алгоритм, используемый в других случаях. Например, код для инвертирования 32-битного значения может быть реализован как комбинация масок и сдвигов, цикла или вызова оптимизированной части машинного кода. Если значение, которое нужно изменить, является константой, комбинация масок и сдвигов будет компилироваться до константы, но использование масок и сдвигов для переменной приведет к раздутому беспорядку, который может быть медленнее, чем цикл. - person supercat; 25.07.2015
comment
Некоторые компиляторы могут распознать, что цикл, написанный на C, при постоянном вводе даст постоянный вывод, но многие этого не сделают. Машинный код может быть в два раза быстрее, чем то, что может создать типичный компилятор, но даже если входной сигнал постоянен, компилятор, скорее всего, не обнаружит, что выход также был постоянным. - person supercat; 25.07.2015
comment
Я знаю, к чему вы клоните :). Я думаю, если вы очень увлечены, вы можете получить 32-битный обратный пример для компиляции в разные алгоритмы (а затем в константы) с помощью частичного оценщика темпа (phoenix.inria.fr/software/past-projects/tempo). Он древний, но вы сделаете счастливыми многих стариков. - person user1666959; 26.07.2015

Думаю, стоит расширить пример @ user1666959. Я отвечу, чтобы предоставить более чистый пример кода. Рассмотрим такой сценарий.

/// Counts odd numbers in range [0;number]
size_t countOdd(size_t number)
{
    size_t result = 0;
    for (size_t i = 0; i <= number; ++i)
    {
        result += (i % 2);
    }
    return result;
}

int main()
{
    return countOdd(5);
}

Если функция не встроена и использует внешние ссылки, она выполнит весь цикл. Представьте, что происходит, когда вы встраиваете его.

int main()
{
    size_t result = 0;
    for (size_t i = 0; i <= 5; ++i)
    {
        result += (i % 2);
    }
    return result;
}

Теперь давайте включим оптимизацию разворачивания цикла. Здесь мы знаем, что он повторяется от 0 до 5, поэтому его можно легко развернуть, удалив нежелательные условия в коде.

int main()
{
    size_t result = 0;
    // iteration 0
    size_t i = 0
    result += (i % 2);
    // iteration 1
    ++i
    result += (i % 2);
    // iteration 2
    ++i
    result += (i % 2);
    // iteration 3
    ++i
    result += (i % 2);
    // iteration 4
    ++i
    result += (i % 2);
    // iteration 5
    ++i
    result += (i % 2);
    return result;
}

Нет условий, уже быстрее, но это еще не все. Мы знаем значение i, так почему бы не передать его напрямую?

int main()
{
    size_t result = 0;
    // iteration 0
    result += (0 % 2);
    // iteration 1
    result += (1 % 2);
    // iteration 2
    result += (2 % 2);
    // iteration 3
    result += (3 % 2);
    // iteration 4
    result += (4 % 2);
    // iteration 5
    result += (5 % 2);
    return result;
}

Еще проще, но эти операции constexpr, мы можем вычислить их во время компиляции.

int main()
{
    size_t result = 0;
    // iteration 0
    result += 0;
    // iteration 1
    result += 1;
    // iteration 2
    result += 0;
    // iteration 3
    result += 1;
    // iteration 4
    result += 0;
    // iteration 5
    result += 1;
    return result;
}

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

int main()
{
    return 3;
}
person Maciej Załucki    schedule 12.11.2020