Mengapa kompiler tidak menyatukan semuanya? [Tutup]

12

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?

Aviv Cohn
sumber
17
IDK tentang Anda, tetapi saya tidak memiliki ratusan GB memori hanya berbaring.
Ampt
2
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.
Doval
5
Anda terlalu generik, apakah "penyusun" berarti "semua penyusun" dan apakah "segalanya" benar-benar berarti "segalanya"? Lalu kemudian jawabannya sederhana, ada situasi di mana Anda tidak bisa sebaris. Rekursi muncul di pikiran.
Otávio Décio
17
Cache locality adalah cara yang jauh lebih penting daripada overhead panggilan fungsi kecil.
SK-logic
3
Apakah peningkatan kinerja benar-benar penting hari ini dengan ratusan GFLOPS kekuatan pemrosesan?
mouviciel

Jawaban:

22

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.

Pemrogram
sumber
3
beberapa panggilan rekursif dapat digarisbawahi (tail tail), tetapi semua dapat diubah menjadi iterasi jika Anda secara opsional menambahkan stack eksplisit
ratchet freak
@ scratchetfreak, Anda juga dapat mengubah beberapa panggilan rekursif non-ekor menjadi satu. Tapi itu bagi saya di bidang yang "sulit" (terutama ketika Anda memiliki fungsi rekursif atau harus menentukan secara dinamis di mana untuk melompat untuk mensimulasikan pengembalian), tetapi itu bukan tidak mungkin (Anda hanya menempatkan kerangka kerja lanjutan dan mengingat saat itu menjadi lebih mudah).
Pemrogram
11

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.

Simon Bergot
sumber
3
Membariskan melalui pengiriman virtual sangat sulit, tetapi bukan tidak mungkin. Beberapa kompiler C ++ dapat melakukannya dalam keadaan tertentu.
bstamour
2
... juga beberapa kompiler JIT (devirtualization).
Frank
@ bstamour Setiap kompiler yang setengah layak dari bahasa apa pun dengan optimisasi yang sesuai pada akan dikirimkan secara statis, yaitu devirtualise, panggilan ke metode yang dinyatakan virtual pada objek yang tipe dinamisnya dapat diketahui pada waktu kompilasi. Ini dapat memfasilitasi inlining jika fase devirtualisation terjadi sebelum (atau lainnya) fase inlining. Tapi ini sepele. Apakah ada hal lain yang Anda maksud? Saya tidak melihat bagaimana "Inlining melalui pengiriman virtual" yang sebenarnya dapat dicapai. Untuk inline, seseorang harus mengetahui jenis statis - yaitu devirtualise - sehingga keberadaan inlining berarti ada adalah tidak ada pengiriman maya
underscore_d
9

Pertama, Anda tidak dapat selalu inline, mis. Fungsi rekursif mungkin tidak selalu tidak dapat dihapus (tetapi program yang berisi definisi rekursif facthanya dengan pencetakan fact(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 CALLinstruksi 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.

Basile Starynkevitch
sumber
1
Sebagai catatan, LLVM / Dentang (dan beberapa kompiler lain) juga mendukung optimasi waktu tautan .
Anda
Saya tahu itu; KPP ada pada abad sebelumnya (IIRC, di beberapa kompiler kepemilikan MIPS setidaknya).
Basile Starynkevitch
7

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 ...

Tom Tanner
sumber
ini tampaknya hanya mengulangi poin yang dibuat dan dijelaskan dalam jawaban sebelumnya yang telah diposting satu jam yang lalu
agas
1
Cache apa? L1? L2? L3? Mana yang lebih penting?
Peter Mortensen
1

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.

m3th0dman
sumber
1

Pertama, ada contoh-contoh sederhana di mana inlining semuanya akan berjalan dengan sangat buruk. Pertimbangkan kode C sederhana ini:

void f1 (void) { printf ("Hello, world\n"); }
void f2 (void) { f1 (); f1 (); f1 (); f1 (); }
void f3 (void) { f2 (); f2 (); f2 (); f2 (); }
...
void f99 (void) { f98 (); f98 (); f98 (); f98 (); }

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.

gnasher729
sumber