Почему возникают накладные расходы при вызове функций?

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


person Kamuela Franco    schedule 03.08.2015    source источник
comment
возможный дубликат Что происходит на языке ассемблера, когда вы вызвать метод/функцию?   -  person Michael Petch    schedule 03.08.2015
comment
О каких этих подпрограммах вы говорите?   -  person Jens    schedule 03.08.2015
comment
Не всегда возможно встроить. Примерами являются рекурсивные функции, виртуальные функции и указатели функций. (иногда их еще можно заинлайнить, но не в общем случае)   -  person Mysticial    schedule 03.08.2015
comment
Также важно отметить, что входные аргументы иногда являются постоянными значениями (жестко закодированные параметры, такие как количество циклов, известное во время компиляции, но различающееся в зависимости от места вызова). inlineинг таких функций предоставляет компилятору эти константные значения, что обеспечивает более агрессивную оптимизацию.   -  person user3528438    schedule 03.08.2015


Ответы (4)


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

Как правило, вызов функции вызывает задержку по двум причинам:

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

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

person Francois Zard    schedule 03.08.2015
comment
1) и 2) не являются нормой. Современные архитектуры довольно хорошо предсказывают выполнение кода. Также обратите внимание, что вызов функции всегда предсказуем, потому что вы знаете, куда идет выполнение, и, следовательно, можете выполнить предварительную выборку кода и заполнить конвейер. Никаких там задержек. - person Jens; 03.08.2015
comment
Они для арки x86 AFAIK - person Francois Zard; 03.08.2015
comment
Нет, x86 CALL в наши дни составляет около одного цикла. - person Jens; 03.08.2015
comment
Соглашение о вызовах также имеет значение. Затертые регистры вызываемого и вызывающего абонентов необходимо сохранять и восстанавливать. Когда вы добавляете корректировку стека и указатели фреймов, зачастую дешевле встраивать небольшие функции даже при оптимизации размера. - person technosaurus; 10.03.2018

Также см. здесь и здесь для обсуждения того, когда встраивание имеет смысл.

  • Встраивание

    В общем, вы можете предложить компилятору только inline функцию, но компилятор может решить иначе. Однако Visual Studio предлагает собственное ключевое слово forceinline. Некоторые функции не могут быть встроены, например. когда они рекурсивны или когда целевая функция не может быть определена во время компиляции (вызовы через таблицы функций, вызовы виртуальных функций в C++).

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

  • Накладные расходы

    Накладные расходы памяти минимальны при использовании функций, потому что вы не дублируете код; встроенный код дублируется на сайт вызова. Накладные расходы на производительность в наши дни незначительны, потому что современные архитектуры действительно хорошо прогнозируют и вызывают всего 1-2 такта.

person Jens    schedule 03.08.2015

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

person Keith Nicholas    schedule 03.08.2015
comment
Почти во всех архитектурах используются соглашения о вызовах на основе регистров, когда код оптимизирован для сохранения данных как можно ближе к выполнению. Возвращаемые значения всегда возвращаются в регистре. - person Jens; 03.08.2015
comment
некоторые есть, но я считаю это оптимизацией. Я много программирую для встраиваемых систем, и у многих из них для этого очень мало места в регистрах. - person Keith Nicholas; 03.08.2015
comment
@Jens x86 по-прежнему использует соглашение о вызовах на основе стека, и большинство других микроконтроллеров по-прежнему используют стек для передачи параметров из-за отсутствия регистров. Возвращаемые значения передаются через регистр только в том случае, если он достаточно мал, чтобы поместиться в 1 или 2 регистра, в противном случае он будет возвращен через стек. - person phuclv; 03.08.2015

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

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

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

person AnT    schedule 03.08.2015