Как говорят другие, сначала вы должны измерить производительность вашей программы, и, вероятно, вы не найдете никакой разницы на практике.
Тем не менее, с концептуального уровня я решил прояснить несколько вещей, которые связаны в вашем вопросе. Во-первых, вы спрашиваете:
Имеют ли значение вызовы функций в современных компиляторах?
Обратите внимание на ключевые слова «функция» и «компиляторы». Ваша цитата тонко отличается:
Помните, что стоимость вызова метода может быть значительной в зависимости от языка.
Это говорит о методах в объектно-ориентированном смысле.
Хотя «функция» и «метод» часто используются взаимозаменяемо, существуют различия, когда речь заходит об их стоимости (о которой вы спрашиваете) и когда речь идет о компиляции (которой вы дали контекст).
В частности, нам нужно знать о статической диспетчеризации и динамической диспетчеризации . Я буду игнорировать оптимизацию на данный момент.
На языке, подобном C, мы обычно вызываем функции со статической диспетчеризацией . Например:
int foo(int x) {
return x + 1;
}
int bar(int y) {
return foo(y);
}
int main() {
return bar(42);
}
Когда компилятор видит вызов foo(y)
, он знает, на какую функцию foo
ссылается это имя, поэтому программа вывода может сразу перейти к foo
функции, что довольно дешево. Вот что означает статическая отправка .
Альтернативой является динамическая диспетчеризация , когда компилятор не знает, какая функция вызывается. В качестве примера, вот некоторый код на Haskell (поскольку эквивалент C был бы грязным!):
foo x = x + 1
bar f x = f x
main = print (bar foo 42)
Здесь bar
функция вызывает свой аргумент f
, который может быть чем угодно. Следовательно, компилятор не может просто скомпилировать bar
инструкцию быстрого перехода, потому что он не знает, куда перейти. Вместо этого код, для которого мы генерируем bar
, f
разыскивает, чтобы выяснить, на какую функцию он указывает, а затем перейти к ней. Вот что означает динамическая отправка .
Оба эти примера предназначены для функций . Вы упомянули методы , которые можно рассматривать как особый стиль динамически отправляемой функции. Например, вот немного Python:
class A:
def __init__(self, x):
self.x = x
def foo(self):
return self.x + 1
def bar(y):
return y.foo()
z = A(42)
bar(z)
y.foo()
Вызов использует динамическую отправку, так как он смотрит вверх значение foo
свойства в y
объекте, и называя все , что он находит; он не знает, что y
будет иметь класс A
, или что A
класс содержит foo
метод, поэтому мы не можем просто перейти к нему.
ОК, это основная идея. Обратите внимание, что статическая отправка выполняется быстрее, чем динамическая, независимо от того, компилируем мы или интерпретируем; при прочих равных условиях Разыменование в любом случае требует дополнительных затрат.
Итак, как это влияет на современные оптимизирующие компиляторы?
Первое, на что нужно обратить внимание, - это то, что статическая диспетчеризация может быть оптимизирована более интенсивно: когда мы знаем, к какой функции мы обращаемся, мы можем делать такие вещи, как встраивание. С динамической диспетчеризацией мы не знаем, что мы прыгаем до времени выполнения, поэтому мы не можем сделать большую оптимизацию.
Во-вторых, в некоторых языках можно определить, куда будут переходить некоторые динамические диспетчеры, и, следовательно, оптимизировать их в статическую диспетчеризацию. Это позволяет нам выполнять другие оптимизации, такие как встраивание и т. Д.
В приведенном выше примере Python такой вывод довольно безнадежен, так как Python позволяет другому коду переопределять классы и свойства, поэтому сложно сделать вывод о том, что будет иметь место во всех случаях.
Если наш язык позволяет нам накладывать больше ограничений, например, ограничивая y
класс A
с помощью аннотации, то мы можем использовать эту информацию для вывода целевой функции. В языках с подклассами (а это почти все языки с классами!) Этого на самом деле недостаточно, поскольку на y
самом деле может иметься другой (под) класс, поэтому нам потребуется дополнительная информация, такая как final
аннотации Java, чтобы точно знать, какая функция будет вызвана.
Haskell не является язык OO, но мы можем сделать вывод , значение f
по встраиванию bar
(который статический отправляются) в main
подставляя foo
для y
. Так как цель foo
in main
статически известна, вызов становится статически распределенным и, вероятно, будет полностью встроен и оптимизирован (поскольку эти функции невелики, компилятор с большей вероятностью их встроит; хотя в целом мы не можем рассчитывать на это) ).
Следовательно, стоимость сводится к:
- Язык отправляет ваш звонок статически или динамически?
- Если это последнее, позволяет ли язык реализации выводить цель, используя другую информацию (например, типы, классы, аннотации, встраивание и т. Д.)?
- Насколько агрессивно можно оптимизировать статическую диспетчеризацию (логическую или иную)?
Если вы используете «очень динамичный» язык, с большим количеством динамической диспетчеризации и несколькими гарантиями, доступными для компилятора, то каждый вызов будет стоить. Если вы используете «очень статичный» язык, то зрелый компилятор будет создавать очень быстрый код. Если вы находитесь между ними, то это может зависеть от вашего стиля кодирования и от того, насколько умна реализация.