Як кажуть інші, спершу слід виміряти ефективність програми і, ймовірно, не знайдеш різниці на практиці.
І все-таки з концептуального рівня я подумав, що я проясню декілька речей, які пов'язані з вашим запитанням. По-перше, ви запитуєте:
Чи все ще важливі витрати на виклики функцій у сучасних компіляторах?
Зауважте ключові слова "функція" та "компілятори". Ваша цитата тонко відрізняється:
Пам'ятайте, що вартість виклику методу може бути значною, залежно від мови.
Мова йде про методи , в об’єктно-орієнтованому сенсі.
Хоча "функцію" та "метод" часто використовують беззмістовно, існують відмінності, коли мова йде про їхню вартість (про яку ви запитуєте) та коли мова йде про компіляцію (що таке контекст, який ви дали).
Зокрема, нам потрібно знати про статичну та динамічну . На даний момент я проігнорую оптимізації.
Мовою, такою як 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 не є мовою ОО, але ми можемо зробити висновок про значення f
, включивши bar
(який статично відправляється) main
, замінивши foo
на y
. Оскільки ціль в foo
in main
статично відома, виклик стає статично відправленим і, ймовірно, буде вбудованим та оптимізованим повністю (оскільки цих функцій мало, компілятор швидше їх вбудовує; хоча ми взагалі не можемо розраховувати на це ).
Отже, вартість зводиться до:
- Мова відправляє ваш дзвінок статично чи динамічно?
- Якщо мова йде про останнє, чи дозволяє мова реалізації реалізувати ціль за допомогою іншої інформації (наприклад, типів, класів, анотацій, вкладок тощо)?
- Наскільки агресивно можна оптимізувати статичну диспетчеризацію (висновок чи інше)?
Якщо ви користуєтеся "дуже динамічною" мовою, з великою кількістю динамічної відправки та кількома гарантіями, доступними для компілятора, то кожен дзвінок буде мати витрати. Якщо ви використовуєте "дуже статичну" мову, то зрілий компілятор створить дуже швидкий код. Якщо ви перебуваєте між ними, то це може залежати від вашого стилю кодування та наскільки розумною є реалізація.