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

Почему функция С++ работает дешево?

Во время чтения Stroustrup "Язык программирования С++" я натолкнулся на это предложение на стр. 108:

"Используемый стиль анализа синтаксиса обычно называется рекурсивным спусканием, он является популярным и простым методом сверху вниз. На языке, таком как С++, в котором вызовы функций относительно дешевы, он также эффективен".

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

4b9b3361

Ответ 1

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

На некоторых других языках (например, Common Lisp, при применении неизвестной вариационной функции) это может быть более сложным.

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

Тем не менее, оптимизация компиляторов способна к изумительным трюкам.

Для многих функциональных языков вызывается функция closure и требует некоторой косвенности (а также передачи закрытых значений).

Некоторые объектно-ориентированные языки (например, Smalltalk) могут включать поиск словаря методов при вызове селектора (на произвольном получателе).

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

Ответ 2

Функциональные вызовы дешевы в С++ по сравнению с большинством других языков по одной причине: С++ построен на концепции функции inline, тогда как (например) java построен на концепции все-есть-виртуальная функция.

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

Даже если функция не включена, компилятор может сделать предположения о том, что делает функция. Например: соглашение о вызове Windows X64 указывает, что регистры R12-R15, XMM6-XMM15 должны быть сохранены вызывающим. При вызове функции компилятор должен генерировать код на сайте вызова для сохранения и восстановления этих регистров. Но если компилятор может доказать, что регистры R12-R15, XMM6-XMM15 не используются вызываемой функцией, такой код можно опустить. Эта оптимизация намного сложнее при вызове виртуальной функции.

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

Наконец, при использовании вызова функции java или функции С++ с ключевым словом virtual, процессор выполнит виртуальную инструкцию call. Разница с прямым вызовом заключается в том, что цель не фиксирована, а сохраняется в памяти. Целевая функция может меняться во время работы программы, а это означает, что ЦП не всегда может предварительно запрограммировать данные в расположении функции. Современные процессоры и JIT-компиляторы имеют различные трюки, чтобы предсказать местоположение целевой функции, но все равно не так быстро, как прямые вызовы.

TL;DR: вызовы функций на С++ бывают быстрыми, потому что С++ реализует inlining и по умолчанию использует прямые вызовы по виртуальным вызовам. Многие другие языки не реализуют inlining, а также С++ и используют виртуальные функции по умолчанию.

Ответ 3

Стоимость вызова функции связана с набором операций, необходимых для перехода от заданной области к другой, то есть от текущего выполнения до области действия другой функции. Рассмотрим следующий код:

void foo(int w) { int x, y, z; ...; }
int main() { int a, b, c; ...; foo(b); ...; }

Выполнение начинается с main(), и некоторые переменные могут быть загружены в регистры/память. Когда вы достигаете foo(), набор переменных, доступных для использования, различен: значения a, b, c недоступны для функции foo(), и в случае, если у вас закончились доступные регистры, сохраненные значения должны быть пропущены в память,

Проблема с регистрами появляется на любом языке. Но некоторые языки нуждаются в более сложных операциях для изменения от области видимости до области видимости: С++ просто подталкивает все, что требуется этой функции, в стек памяти, поддерживая указатели на окружающие области (в этом случае при запуске foo() вы сможете для достижения определения w в области main().

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