Seberapa banyak panggilan fungsi berdampak pada kinerja?

13

Mengekstrak fungsionalitas menjadi metode atau fungsi adalah suatu keharusan untuk kode modularitas, keterbacaan dan interoperabilitas, terutama di OOP.

Tetapi ini berarti lebih banyak fungsi panggilan akan dilakukan.

Bagaimana memecah kode kami menjadi metode atau fungsi sebenarnya memengaruhi kinerja dalam bahasa * modern ?

* Yang paling populer: C, Java, C ++, C #, Python, JavaScript, Ruby ...

dabadaba
sumber
1
Setiap implementasi bahasa yang bernilai, telah melakukan inlining selama beberapa dekade sekarang, saya pikir. TKI, ongkosnya tepat 0.
Jörg W Mittag
1
"lebih banyak panggilan fungsi akan dibuat" seringkali tidak benar karena banyak dari panggilan itu akan dioptimalkan oleh berbagai kompiler / interpreter yang memproses kode Anda dan menguraikan hal-hal. Jika bahasa Anda tidak memiliki optimasi seperti ini, saya mungkin tidak menganggapnya modern.
Ixrec
2
Bagaimana pengaruhnya terhadap kinerja? Ini akan membuatnya lebih cepat, atau lebih lambat, atau tidak mengubahnya, tergantung pada bahasa spesifik apa yang Anda gunakan dan apa struktur kode aktual dan mungkin pada versi kompiler yang Anda gunakan dan mungkin bahkan platform apa yang Anda gunakan. sedang berjalan. Setiap jawaban yang Anda dapatkan akan menjadi beberapa variasi dari ketidakpastian ini, dengan lebih banyak kata dan lebih banyak bukti pendukung.
GrandOpener
1
Dampaknya, jika ada, sangat kecil sehingga Anda, seseorang, tidak akan pernah menyadarinya. Ada hal-hal lain yang jauh lebih penting untuk dikhawatirkan. Seperti apakah tab harus 5 atau 7 spasi.
MetaFight

Jawaban:

21

Mungkin. Compiler mungkin memutuskan "hei, fungsi ini hanya dipanggil beberapa kali, dan saya seharusnya mengoptimalkan untuk kecepatan, jadi saya hanya akan sebaris fungsi ini". Pada dasarnya, kompiler akan menggantikan pemanggilan fungsi dengan tubuh fungsi. Sebagai contoh, kode sumber akan terlihat seperti ini.

void DoSomething()
{
   a = a + 1;
   DoSomethingElse(a);
}

void DoSomethingElse(int a)
{
   b = a + 3;
}

Kompilator memutuskan untuk sebaris DoSomethingElse, dan kode menjadi

void DoSomething()
{
   a = a + 1;
   b = a + 3;
}

Ketika fungsi tidak diuraikan, ya ada hit kinerja untuk melakukan panggilan fungsi. Namun, ini adalah hit yang sangat kecil sehingga hanya kode kinerja sangat tinggi yang akan mengkhawatirkan panggilan fungsi. Dan pada proyek-proyek semacam itu, kodenya biasanya ditulis dalam pertemuan.

Panggilan fungsi (tergantung pada platform) biasanya melibatkan beberapa instruksi, dan itu termasuk menyimpan / mengembalikan tumpukan. Beberapa panggilan fungsi terdiri dari instruksi melompat dan kembali.

Tetapi ada hal-hal lain yang mungkin memengaruhi kinerja fungsi panggilan. Fungsi yang dipanggil mungkin tidak dimuat ke cache prosesor, menyebabkan cache gagal dan memaksa pengontrol memori untuk mengambil fungsi dari RAM utama. Ini dapat menyebabkan hit besar untuk kinerja.

Singkatnya: panggilan fungsi mungkin atau mungkin tidak mempengaruhi kinerja. Satu-satunya cara untuk mengetahui adalah membuat profil kode Anda. Jangan mencoba menebak di mana letak kode lambat, karena kompiler dan perangkat keras memiliki beberapa trik luar biasa. Profil kode untuk mendapatkan lokasi titik lambat.

Chendrix
sumber
1
Saya telah melihat dengan kompiler modern (gcc, dentang) dalam situasi di mana saya benar-benar peduli bahwa mereka membuat kode yang sangat buruk untuk loop di dalam fungsi yang besar . Mengekstrak loop menjadi fungsi statis tidak membantu karena inlining. Mengekstrak loop ke fungsi eksternal yang dibuat dalam beberapa kasus peningkatan kecepatan yang signifikan (dapat diukur dalam tolok ukur).
gnasher729
1
Saya akan mendukung hal ini dan mengatakan OP harus berhati-hati tentang Pengoptimalan Prematur
Patrick
1
@ Patrick Bingo. Jika Anda akan mengoptimalkan, gunakan profiler untuk melihat di mana bagian yang lambat. Jangan menebak. Anda biasanya bisa merasakan di mana bagian yang lambat mungkin, tetapi konfirmasikan dengan profiler.
CHendrix
@ gnasher729 Untuk mengatasi masalah khusus itu, orang akan membutuhkan lebih dari sekadar profiler - orang harus belajar membaca kode mesin yang dibongkar juga. Meskipun ada optimasi prematur, tidak ada yang namanya pembelajaran prematur (setidaknya dalam pengembangan perangkat lunak).
rwong
Anda mungkin memiliki masalah ini jika Anda memanggil fungsi satu juta kali, tetapi Anda lebih cenderung memiliki masalah lain yang memiliki dampak yang jauh lebih besar.
Michael Shaw
5

Ini adalah masalah implementasi dari kompiler atau runtime (dan opsinya) dan tidak dapat dikatakan dengan pasti.

Di dalam C dan C ++, beberapa kompiler akan inline panggilan berdasarkan pada pengaturan optimisasi - ini dapat dilihat secara sepele dengan memeriksa rakitan yang dihasilkan ketika melihat alat seperti https://gcc.godbolt.org/

Bahasa lain, seperti Java, memiliki ini sebagai bagian dari runtime. Ini adalah bagian dari JIT dan diuraikan dalam pertanyaan SO ini . Dalam tampilan paticular pada opsi JVM untuk HotSpot

-XX:InlineSmallCode=n Sebariskan metode yang dikompilasi sebelumnya hanya jika ukuran kode asli yang dihasilkan kurang dari ini. Nilai default bervariasi dengan platform tempat JVM berjalan.
-XX:MaxInlineSize=35 Ukuran bytecode maksimum metode yang akan diuraikan.
-XX:FreqInlineSize=n Ukuran bytecode maksimum dari metode yang sering dieksekusi untuk diuraikan. Nilai default bervariasi dengan platform tempat JVM berjalan.

Jadi ya, kompiler JIT HotSpot akan inline metode yang memenuhi kriteria tertentu.

The dampak dari ini, sulit untuk menentukan karena setiap JVM (atau compiler) dapat melakukan sesuatu yang berbeda dan mencoba untuk menjawab dengan stroke luas bahasa hampir pasti salah. Dampaknya hanya dapat ditentukan dengan benar dengan membuat profil kode di lingkungan berjalan yang sesuai dan memeriksa output yang dikompilasi.

Ini dapat dilihat sebagai pendekatan yang salah arah dengan CPython tidak inlining, tetapi Jython (Python berjalan di JVM) memiliki beberapa panggilan yang digarisbawahi. Demikian juga MRI Ruby tidak inlining sementara JRuby mau, dan ruby2c yang merupakan transpiler untuk ruby ​​ke C ... yang kemudian bisa inlining atau tidak tergantung pada opsi kompiler C yang dikompilasi.

Bahasa tidak sebaris. Implementasi dapat .

pengguna227864
sumber
5

Anda mencari kinerja di tempat yang salah. Masalah dengan panggilan fungsi bukan karena harganya yang mahal. Ada masalah lain. Panggilan fungsi bisa benar-benar gratis, dan Anda masih memiliki masalah lain ini.

Itu adalah fungsi seperti kartu kredit. Karena Anda dapat dengan mudah menggunakannya, Anda cenderung menggunakannya lebih dari yang seharusnya. Misalkan Anda menyebutnya 20% lebih dari yang Anda butuhkan. Kemudian, perangkat lunak besar yang khas berisi beberapa lapisan, masing-masing fungsi panggilan di lapisan di bawah, sehingga faktor 1.2 dapat diperparah dengan jumlah lapisan. (Misalnya, jika ada lima lapisan, dan setiap lapisan memiliki faktor perlambatan 1,2, faktor perlambatan majemuk adalah 1,2 ^ 5 atau 2,5.) Ini hanya satu cara untuk memikirkannya.

Ini tidak berarti Anda harus menghindari panggilan fungsi. Maksudnya adalah, ketika kode aktif dan berjalan, Anda harus tahu cara menemukan dan menghilangkan pemborosan. Ada banyak saran bagus tentang cara melakukan ini di situs stackexchange. Ini memberikan salah satu kontribusi saya.

TAMBAH: Contoh kecil. Suatu kali saya bekerja dalam tim perangkat lunak di pabrik yang melacak serangkaian perintah kerja atau "pekerjaan". Ada fungsi JobDone(idJob)yang bisa mengetahui apakah suatu pekerjaan dilakukan. Suatu pekerjaan dilakukan ketika semua sub-tugasnya dilakukan, dan masing-masing dilakukan ketika semua sub-operasinya selesai. Semua hal ini disimpan dalam database relasional. Panggilan tunggal ke fungsi lain dapat mengekstrak semua informasi itu, yang JobDonedisebut fungsi lain itu, melihat apakah pekerjaan itu dilakukan, dan membuang sisanya. Maka orang dapat dengan mudah menulis kode seperti ini:

while(!JobDone(idJob)){
    ...
}

atau

foreach(idJob in jobs){
    if (JobDone(idJob)){
        ...
    }
}

Lihat intinya? Fungsinya sangat "kuat" dan mudah dipanggil sehingga terlalu banyak dipanggil. Jadi masalah kinerja bukan instruksi masuk dan keluar dari fungsi. Itu perlu ada cara yang lebih langsung untuk mengetahui apakah pekerjaan telah dilakukan. Sekali lagi, kode ini dapat tertanam dalam ribuan baris kode yang tidak bersalah. Mencoba memperbaikinya terlebih dahulu adalah apa yang semua orang coba lakukan, tapi itu seperti mencoba melemparkan anak panah di ruangan gelap. Yang Anda butuhkan adalah menjalankannya, dan kemudian biarkan "kode lambat" memberi tahu Anda apa itu, hanya dengan mengambil waktu. Untuk itu saya menggunakan jeda acak .

Mike Dunlavey
sumber
1

Saya pikir itu benar-benar tergantung pada bahasa dan fungsi. Sementara kompiler c dan c ++ dapat menyejajarkan banyak fungsi, ini bukan kasus untuk Python atau Java.

Meskipun saya tidak tahu rincian spesifik untuk java (kecuali bahwa setiap metode virtual tetapi saya menyarankan Anda untuk memeriksa dokumentasi dengan lebih baik), dengan Python saya yakin bahwa tidak ada inlining, tidak ada optimasi pengulangan ekor dan panggilan fungsi yang cukup mahal.

Fungsi-fungsi Python pada dasarnya adalah objek yang dapat dieksekusi (dan ternyata Anda juga dapat mendefinisikan metode panggilan () untuk membuat instance objek menjadi fungsi). Ini berarti ada cukup banyak overhead dalam memanggil mereka ...

TAPI

ketika Anda mendefinisikan variabel di dalam fungsi, interpreter menggunakan LOADFAST alih-alih instruksi LOAD normal dalam bytecode, membuat kode Anda lebih cepat ...

Hal lain adalah ketika Anda mendefinisikan objek yang dapat dipanggil, pola seperti memoisasi dimungkinkan dan mereka secara efektif dapat mempercepat perhitungan Anda (dengan biaya menggunakan lebih banyak memori). Pada dasarnya itu selalu merupakan trade off. Biaya fungsi panggilan juga tergantung pada parameter, karena mereka menentukan berapa banyak barang yang sebenarnya harus Anda salin di stack (sehingga dalam c / c ++ adalah praktik umum untuk melewatkan parameter besar seperti struktur dengan pointer / referensi alih-alih berdasarkan nilai).

Saya pikir pertanyaan Anda dalam praktik terlalu luas untuk dijawab sepenuhnya di stackexchange.

Apa yang saya sarankan Anda lakukan adalah mulai dengan satu bahasa dan mempelajari dokumentasi lanjutan untuk memahami bagaimana pemanggilan fungsi dilaksanakan oleh bahasa tertentu.

Anda akan terkejut dengan berapa banyak hal yang akan Anda pelajari dalam proses ini.

Jika Anda memiliki masalah khusus, lakukan pengukuran / profiling dan tentukan cuaca lebih baik untuk membuat fungsi atau menyalin / menempelkan kode yang setara.

jika Anda mengajukan pertanyaan yang lebih spesifik, saya pikir akan lebih mudah mendapatkan jawaban yang lebih spesifik.

ingframin
sumber
Mengutip Anda: "Saya pikir pertanyaan Anda dalam praktik terlalu luas untuk dijawab sepenuhnya di stackexchange." Bagaimana saya bisa mempersempitnya? Saya ingin melihat beberapa data aktual yang mewakili dampak panggilan fungsi dalam kinerja. Saya tidak peduli bahasa apa, saya hanya ingin melihat penjelasan yang lebih terperinci, didukung dengan data jika memungkinkan, seperti yang saya katakan.
dabadaba
Intinya tergantung pada bahasanya. Dalam C dan C ++, jika fungsinya digarisbawahi, dampaknya adalah 0. Jika tidak digarisbawahi, itu tergantung pada parameternya, apakah itu ada dalam cache atau tidak, dll ...
ingframin
1

Saya mengukur overhead panggilan fungsi C ++ langsung dan virtual pada Xenon PowerPC beberapa waktu lalu .

Fungsi-fungsi yang dimaksud memiliki parameter tunggal dan pengembalian tunggal, sehingga lewat parameter terjadi pada register.

Singkatnya, overhead panggilan fungsi langsung (non-virtual) adalah sekitar 5,5 nanodetik, atau siklus 18 jam, dibandingkan dengan panggilan fungsi sebaris. Overhead panggilan fungsi virtual adalah 13,2 nanodetik, atau 42 siklus clock, dibandingkan dengan inline.

Pengaturan waktu ini kemungkinan berbeda pada keluarga prosesor yang berbeda. Kode pengujian saya ada di sini ; Anda dapat menjalankan percobaan yang sama pada perangkat keras Anda. Gunakan timer presisi tinggi seperti rdtsc untuk implementasi CFastTimer Anda; waktu sistem () hampir tidak cukup tepat.

Crashworks
sumber