Terkadang kompiler memanggil fungsi inline. Itu berarti bahwa mereka memindahkan kode fungsi yang dipanggil ke fungsi panggilan. Hal ini membuat segalanya sedikit lebih cepat karena tidak perlu mendorong dan mengeluarkan barang dari dan ke stack panggilan.
Jadi pertanyaan saya adalah, mengapa kompiler tidak mengatur semuanya? Saya berasumsi itu akan membuat eksekusi lebih cepat.
Satu-satunya alasan yang bisa saya pikirkan adalah executable yang jauh lebih besar, tetapi apakah ini penting hari ini dengan ratusan GB memori? Bukankah peningkatan kinerja itu sepadan?
Apakah ada alasan lain mengapa kompiler tidak hanya sebaris semua panggilan fungsi?
optimization
compiler
Aviv Cohn
sumber
sumber
Isn't the improved performance worth it?
Untuk metode yang akan menjalankan loop 100 kali dan menghasilkan angka yang serius, overhead untuk memindahkan 2 atau 3 argumen ke register CPU tidak ada artinya.Jawaban:
Catatan pertama bahwa satu efek utama dari inline adalah memungkinkan pengoptimalan lebih lanjut dilakukan di situs panggilan.
Untuk pertanyaan Anda: ada hal-hal yang sulit atau bahkan mustahil untuk di sebaris:
perpustakaan yang terhubung secara dinamis
fungsi yang ditentukan secara dinamis (pengiriman dinamis, dipanggil melalui pointer fungsi)
fungsi rekursif (rekursi ekor bisa)
fungsi di mana Anda tidak memiliki kode (tetapi optimasi waktu tautan memungkinkan ini untuk sebagian dari mereka)
Maka inlining tidak hanya memiliki efek menguntungkan:
executable yang lebih besar berarti lebih banyak tempat disk dan waktu muat yang lebih besar
executable yang lebih besar berarti peningkatan tekanan cache (perhatikan bahwa inlining fungsi yang cukup kecil seperti getter sederhana dapat mengurangi ukuran yang dapat dieksekusi dan tekanan cache)
Dan akhirnya, untuk fungsi-fungsi yang membutuhkan waktu yang tidak sepele untuk dieksekusi, keuntungannya tidak sebanding dengan rasa sakitnya.
sumber
Keterbatasan utama adalah polimorfisme runtime. Jika ada pengiriman dinamis yang terjadi ketika Anda menulis
foo.bar()
maka tidak mungkin untuk sebaris metode panggilan. Ini menjelaskan mengapa kompiler tidak mengatur semuanya.Panggilan rekursif juga tidak dapat dengan mudah diuraikan.
Inlining modul silang juga sulit dilakukan karena alasan teknis (rekompilasi tambahan tidak mungkin dilakukan sebelumnya)
Namun, kompiler melakukan banyak hal.
sumber
Pertama, Anda tidak dapat selalu inline, mis. Fungsi rekursif mungkin tidak selalu tidak dapat dihapus (tetapi program yang berisi definisi rekursif
fact
hanya dengan pencetakanfact(8)
bisa digariskan).Maka, sebaris tidak selalu bermanfaat. Jika kompiler inline begitu banyak sehingga kode hasil cukup besar untuk memiliki bagian panasnya tidak sesuai misalnya cache instruksi L1, itu mungkin jauh lebih lambat daripada versi non-inline (yang akan dengan mudah sesuai dengan cache L1) ... Juga, prosesor terbaru sangat cepat dalam menjalankan
CALL
instruksi mesin (setidaknya ke lokasi yang diketahui, yaitu panggilan langsung, bukan panggilan melalui pointer).Akhirnya, inlining penuh membutuhkan analisis seluruh program. Ini mungkin tidak mungkin (atau terlalu mahal). Dengan C atau C ++ yang dikompilasi oleh GCC (dan juga dengan Dentang / LLVM ) Anda perlu mengaktifkan optimasi tautan-waktu (dengan mengkompilasi dan menghubungkan dengan misalnya
g++ -flto -O2
) dan yang membutuhkan waktu kompilasi yang cukup banyak.sumber
Mengejutkan meskipun tampaknya, memadukan semuanya tidak selalu mengurangi waktu eksekusi. Peningkatan ukuran kode Anda dapat mempersulit CPU untuk menyimpan semua kode Anda dalam cache sekaligus. Kehilangan cache pada kode Anda menjadi lebih mungkin dan miss cache lebih mahal. Ini menjadi jauh lebih buruk jika fungsi yang berpotensi inline Anda besar.
Saya mengalami peningkatan kinerja yang nyata dari waktu ke waktu dengan mengambil potongan besar kode yang ditandai sebagai 'inline' dari file header, memasukkannya ke dalam kode sumber, jadi kode itu hanya di satu tempat daripada di setiap situs panggilan. Kemudian cache CPU digunakan lebih baik dan Anda juga mendapatkan waktu kompilasi yang lebih baik ...
sumber
Memasukkan semuanya tidak berarti hanya meningkatkan konsumsi memori disk tetapi juga meningkatkan konsumsi memori internal yang tidak begitu banyak. Ingat bahwa kode juga bergantung pada memori dalam segmen kode; jika suatu fungsi dipanggil dari 10.000 tempat (katakanlah yang dari perpustakaan standar dalam proyek yang cukup besar), maka kode untuk fungsi tersebut menempati 10.000 kali lebih banyak memori internal.
Alasan lain mungkin adalah kompiler JIT; jika semuanya inline maka tidak ada hot spot untuk dikompilasi secara dinamis.
sumber
Pertama, ada contoh-contoh sederhana di mana inlining semuanya akan berjalan dengan sangat buruk. Pertimbangkan kode C sederhana ini:
Coba tebak apa yang akan dilakukan segala sesuatu pada Anda.
Selanjutnya, Anda membuat asumsi bahwa inlining akan membuat segalanya lebih cepat. Kadang demikian, tetapi tidak selalu. Salah satu alasannya adalah bahwa kode yang cocok dengan cache instruksi berjalan jauh lebih cepat. Jika saya memanggil fungsi dari 10 tempat, saya akan selalu menjalankan kode yang ada di cache instruksi. Jika itu sebaris, maka salinannya ada di semua tempat dan berjalan jauh lebih lambat.
Ada masalah lain: Inlining menghasilkan fungsi yang sangat besar. Fungsi besar jauh lebih sulit untuk dioptimalkan. Saya mendapat banyak keuntungan dalam kode kritis kinerja dengan menyembunyikan fungsi ke dalam file terpisah untuk mencegah kompiler menggarisbawahi mereka. Akibatnya, kode yang dihasilkan untuk fungsi-fungsi ini jauh lebih baik ketika mereka disembunyikan.
BTW. Saya tidak memiliki "ratusan GB memori". Komputer karya saya bahkan tidak memiliki "ruang hard drive ratusan GB". Dan jika aplikasi saya di mana "ratusan GB memori", itu akan memakan waktu 20 menit hanya untuk memuat aplikasi ke memori.
sumber