Seperti yang orang lain katakan, Anda harus mengukur kinerja program Anda terlebih dahulu, dan mungkin tidak akan menemukan perbedaan dalam praktiknya.
Namun, dari level konseptual, saya pikir saya akan menjelaskan beberapa hal yang tergabung dalam pertanyaan Anda. Pertama, Anda bertanya:
Apakah biaya panggilan fungsi masih penting dalam kompiler modern?
Perhatikan kata-kata kunci "fungsi" dan "penyusun". Kutipan Anda berbeda secara subtil:
Ingat bahwa biaya pemanggilan metode bisa signifikan, tergantung pada bahasanya.
Ini berbicara tentang metode , dalam arti berorientasi objek.
Sementara "fungsi" dan "metode" sering digunakan secara bergantian, ada perbedaan dalam hal biayanya (yang Anda tanyakan) dan ketika menyangkut kompilasi (yang merupakan konteks yang Anda berikan).
Secara khusus, kita perlu tahu tentang pengiriman statis vs pengiriman dinamis . Saya akan mengabaikan optimisasi untuk saat ini.
Dalam bahasa seperti C, kami biasanya memanggil fungsi dengan pengiriman statis . Sebagai contoh:
int foo(int x) {
return x + 1;
}
int bar(int y) {
return foo(y);
}
int main() {
return bar(42);
}
Ketika kompiler melihat panggilan foo(y)
, ia tahu fungsi apa yang foo
merujuk nama itu, sehingga program keluaran bisa langsung melompat ke foo
fungsi, yang cukup murah. Itulah arti pengiriman statis .
Alternatifnya adalah pengiriman dinamis , di mana kompiler tidak tahu fungsi mana yang dipanggil. Sebagai contoh, inilah beberapa kode Haskell (karena setara C akan berantakan!):
foo x = x + 1
bar f x = f x
main = print (bar foo 42)
Di sini bar
fungsinya memanggil argumennya f
, yang bisa berupa apa saja. Karenanya kompiler tidak bisa hanya mengkompilasi bar
ke instruksi lompatan cepat, karena ia tidak tahu ke mana harus melompat. Sebagai gantinya, kode yang kita hasilkan untuk bar
dereference akan f
mencari tahu fungsi yang ditunjuknya, lalu beralih ke sana. Itulah arti pengiriman dinamis .
Kedua contoh tersebut adalah untuk fungsi . Anda menyebutkan metode , yang dapat dianggap sebagai gaya fungsi khusus yang dikirim secara dinamis. Sebagai contoh, inilah beberapa 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)
The y.foo()
panggilan menggunakan dispatch dinamis, karena itu mencari nilai dari foo
properti di y
objek, dan memanggil apa pun yang ditemukan; tidak tahu bahwa y
akan ada kelas A
, atau bahwa A
kelas berisi foo
metode, jadi kita tidak bisa langsung langsung ke sana.
OK, itu ide dasarnya. Perhatikan bahwa pengiriman statis lebih cepat daripada pengiriman dinamis terlepas dari apakah kami mengkompilasi atau menafsirkan; semuanya sama. Dereferencing dikenakan biaya tambahan.
Jadi bagaimana hal ini memengaruhi kompiler modern dan optimal?
Hal pertama yang perlu diperhatikan adalah pengiriman statis dapat dioptimalkan lebih berat: ketika kita tahu ke mana fungsi kita melompat, dapat melakukan hal-hal seperti inlining. Dengan pengiriman dinamis, kami tidak tahu bahwa kami akan melompat sampai waktu berjalan, jadi tidak banyak optimasi yang dapat kami lakukan.
Kedua, dimungkinkan dalam beberapa bahasa untuk menyimpulkan di mana beberapa pengiriman dinamis akan berakhir melompat, dan karenanya mengoptimalkannya menjadi pengiriman statis. Ini memungkinkan kami melakukan optimisasi lain seperti inlining, dll.
Dalam contoh Python di atas, kesimpulan semacam itu sangat tidak ada harapan, karena Python memungkinkan kode lain untuk mengesampingkan kelas dan properti, sehingga sulit untuk menyimpulkan banyak hal yang akan berlaku dalam semua kasus.
Jika bahasa kita memungkinkan kita memaksakan lebih banyak pembatasan, misalnya dengan membatasi y
kelas A
menggunakan anotasi, maka kita dapat menggunakan informasi itu untuk menyimpulkan fungsi target. Dalam bahasa dengan subclassing (yang hampir semua bahasa dengan kelas!) Itu sebenarnya tidak cukup, karena y
mungkin sebenarnya memiliki kelas (sub) yang berbeda, jadi kita akan membutuhkan informasi tambahan seperti final
anotasi Java untuk mengetahui dengan tepat fungsi mana yang akan dipanggil.
Haskell bukan bahasa OO, tapi kami dapat menyimpulkan nilai f
oleh inlining bar
(yang statis dikirim) ke main
, menggantikan foo
untuk y
. Karena target foo
in main
diketahui secara statis, panggilan menjadi dikirim secara statis, dan mungkin akan diuraikan dan dioptimalkan sepenuhnya (karena fungsi-fungsi ini kecil, kompiler lebih cenderung untuk menyejajarkannya, meskipun kita tidak dapat mengandalkannya secara umum ).
Karenanya biaya turun ke:
- Apakah bahasa mengirim panggilan Anda secara statis atau dinamis?
- Jika yang terakhir, apakah bahasa memungkinkan implementasi menyimpulkan target menggunakan informasi lain (misalnya jenis, kelas, anotasi, sebaris, dll.)?
- Seberapa agresif pengiriman statis (disimpulkan atau tidak) dioptimalkan?
Jika Anda menggunakan bahasa "sangat dinamis", dengan banyak pengiriman dinamis dan sedikit jaminan yang tersedia untuk kompiler, maka setiap panggilan akan dikenai biaya. Jika Anda menggunakan bahasa "sangat statis", maka kompiler yang matang akan menghasilkan kode yang sangat cepat. Jika Anda berada di antara keduanya, maka itu dapat bergantung pada gaya pengkodean Anda dan seberapa pintar implementasinya.