Подтвердить что ты не робот

Насколько глубоки встроенные функции компиляторов?

Скажем, у меня есть некоторые функции, каждая из двух простых строк кода, и они называют друг друга следующим образом: A вызывает B вызывает C вызовы D... вызывает K. (В основном это длинная серия коротких вызовов функций.) Насколько глубоко компиляторы обычно идут в дереве вызовов, чтобы встроить эти функции?

4b9b3361

Ответ 1

Вопрос не имеет смысла.

Если вы думаете о вложениях и их последствиях, вы это осознаете:

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

При принятии решения о том, следует ли встроить или нет, компилятор, таким образом, выполняет действие балансировки между созданным потенциальным наворотом и ожидаемым коэффициентом усиления скорости. Этот эффект балансировки зависит от параметров: gcc -O3 означает оптимизацию скорости, а -Oz означает оптимизацию размера, при вставке они имеют квазипротивоположное поведение!

Поэтому важно не "уровень вложенности" - это количество команд (возможно, взвешенных, поскольку не все они созданы равными).

Это означает, что простая функция пересылки:

int foo(int a, int b) { return foo(a, b, 3); }

по существу является "прозрачным" с точки зрения инкрустации.

С другой стороны, функция, считающая сотню строк кода, вряд ли будет встроена. За исключением того, что a static свободные функции, называемые только один раз, квази систематически инкрементированы, поскольку в этом случае он не создает никакого дублирования.

Из этих двух примеров мы узнаем, как ведут себя эвристики:

  • чем меньше команд имеет функция, тем лучше для inling
  • чем реже он называется, тем лучше для inlining

После этого они являются параметрами, которые вы должны иметь возможность влиять так или иначе (MSVC как __force_inline, который сильно указывает на inling, gcc, поскольку они -finline-limit флаг, чтобы "поднять" позицию на количество команд и т.д.)


По касательной: знаете ли вы о частичном встраивании?

Он был введен в gcc в 4.6. Идея, как следует из названия, состоит в том, чтобы частично включить функцию. В основном, чтобы избежать накладных расходов на вызов функции, когда функция "охраняется" и может (в некоторых случаях) вернуться почти сразу.

Например:

void foo(Bar* x) {
  if (not x) { return; } // null pointer, pfff!

  // ... BIG BLOC OF STATEMENTS ...
}

void bar(Bar* x) {
  // DO 1
  foo(x);
  // DO 2
}

может быть оптимизирован как:

void [email protected](Bar* x) {
  // ... BIG BLOC OF STATEMENTS ...
}

void bar(Bar* x) {
  // DO 1
  if (x) { [email protected](x); }
  // DO 2
}

Конечно, еще раз применяются эвристики для наложения, но они применяются более дискриминационно!


И, наконец, если вы не используете WPO (Оптимизация всей программы) или LTO (Оптимизация времени связи), функции могут быть включены только в том случае, если их определение находится в одном и том же модуле перевода (Translation Unit), который находится на сайте вызова.

Ответ 2

Я видел, что компиляторы встроены в более чем 5 функций. Но в какой-то момент он в основном становится компромиссом по эффективности пространства, который делает компилятор. В этом аспекте каждый компилятор отличается. Visual Studio очень консервативна с inlining. GCC (под -03) и компилятор Intel любят встроенный...