Как говорят другие, сначала вы должны измерить производительность вашей программы, и, вероятно, вы не найдете никакой разницы на практике.
Тем не менее, с концептуального уровня я решил прояснить несколько вещей, которые связаны в вашем вопросе. Во-первых, вы спрашиваете:
Имеют ли значение вызовы функций в современных компиляторах?
Обратите внимание на ключевые слова «функция» и «компиляторы». Ваша цитата тонко отличается:
Помните, что стоимость вызова метода может быть значительной в зависимости от языка.
Это говорит о методах в объектно-ориентированном смысле.
Хотя «функция» и «метод» часто используются взаимозаменяемо, существуют различия, когда речь заходит об их стоимости (о которой вы спрашиваете) и когда речь идет о компиляции (которой вы дали контекст).
В частности, нам нужно знать о статической диспетчеризации и динамической диспетчеризации . Я буду игнорировать оптимизацию на данный момент.
На языке, подобном 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. Так как цель fooin mainстатически известна, вызов становится статически распределенным и, вероятно, будет полностью встроен и оптимизирован (поскольку эти функции невелики, компилятор с большей вероятностью их встроит; хотя в целом мы не можем рассчитывать на это) ).
Следовательно, стоимость сводится к:
- Язык отправляет ваш звонок статически или динамически?
- Если это последнее, позволяет ли язык реализации выводить цель, используя другую информацию (например, типы, классы, аннотации, встраивание и т. Д.)?
- Насколько агрессивно можно оптимизировать статическую диспетчеризацию (логическую или иную)?
Если вы используете «очень динамичный» язык, с большим количеством динамической диспетчеризации и несколькими гарантиями, доступными для компилятора, то каждый вызов будет стоить. Если вы используете «очень статичный» язык, то зрелый компилятор будет создавать очень быстрый код. Если вы находитесь между ними, то это может зависеть от вашего стиля кодирования и от того, насколько умна реализация.