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

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

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

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

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

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

Ответ 1

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

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

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


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

Ответ 2

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

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

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

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

Ответ 3

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

Вложения решений в компиляторе имеют очень мало общего с "Создание простой ветки Predictor Happy". Или менее запутанным.

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

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

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

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

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

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

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

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

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

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

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