Sederhananya, apa itu optimasi tail-call?
Lebih khusus, potongan kode kecil apa yang bisa diterapkan, dan di mana tidak, dengan penjelasan mengapa?
Sederhananya, apa itu optimasi tail-call?
Lebih khusus, potongan kode kecil apa yang bisa diterapkan, dan di mana tidak, dengan penjelasan mengapa?
Jawaban:
Optimasi tail-call adalah di mana Anda dapat menghindari alokasi bingkai stack baru untuk suatu fungsi karena fungsi panggilan hanya akan mengembalikan nilai yang didapatnya dari fungsi yang dipanggil. Penggunaan yang paling umum adalah tail-recursion, di mana fungsi rekursif yang ditulis untuk mengambil keuntungan dari optimasi tail-call dapat menggunakan ruang stack yang konstan.
Skema adalah salah satu dari sedikit bahasa pemrograman yang menjamin dalam spesifikasi bahwa setiap implementasi harus menyediakan optimasi ini (JavaScript juga, dimulai dengan ES6) , jadi berikut adalah dua contoh fungsi faktorial dalam Skema:
Fungsi pertama bukanlah rekursif ekor karena ketika panggilan rekursif dibuat, fungsi tersebut perlu melacak perkalian yang harus dilakukan dengan hasil setelah panggilan kembali. Dengan demikian, tumpukan terlihat sebagai berikut:
Sebaliknya, jejak tumpukan untuk faktorial rekursif ekor terlihat sebagai berikut:
Seperti yang Anda lihat, kita hanya perlu melacak jumlah data yang sama untuk setiap panggilan ke fakta karena kita hanya mengembalikan nilai yang kita dapatkan sampai ke atas. Ini berarti bahwa bahkan jika saya menelepon (fakta 1000000), saya hanya membutuhkan jumlah ruang yang sama dengan (fakta 3). Ini tidak terjadi dengan fakta non-ekor-rekursif, dan karena nilai-nilai besar seperti itu dapat menyebabkan stack overflow.
sumber
Mari kita berjalan melalui contoh sederhana: fungsi faktorial diimplementasikan dalam C.
Kita mulai dengan definisi rekursif yang jelas
Suatu fungsi berakhir dengan panggilan ekor jika operasi terakhir sebelum fungsi kembali adalah panggilan fungsi lain. Jika panggilan ini memanggil fungsi yang sama, itu adalah ekor-rekursif.
Meskipun
fac()
terlihat rekursif pada pandangan pertama, tidak seperti apa yang sebenarnya terjadiyaitu operasi terakhir adalah perkalian dan bukan pemanggilan fungsi.
Namun, dimungkinkan untuk menulis ulang
fac()
menjadi ekor-rekursif dengan meneruskan nilai akumulasi ke rantai panggilan sebagai argumen tambahan dan hanya meneruskan hasil akhir lagi sebagai nilai pengembalian:Sekarang, mengapa ini berguna? Karena kita segera kembali setelah tail tail, kita dapat membuang stackframe sebelumnya sebelum memanggil fungsi pada posisi tail, atau, jika terjadi fungsi rekursif, gunakan kembali stackframe apa adanya.
Optimalisasi panggilan ekor mengubah kode rekursif kami menjadi
Ini bisa disimpulkan
fac()
dan kita sampaiyang setara dengan
Seperti yang dapat kita lihat di sini, pengoptimal yang cukup canggih dapat menggantikan rekursi ekor dengan iterasi, yang jauh lebih efisien karena Anda menghindari overhead panggilan fungsi dan hanya menggunakan jumlah ruang stack yang konstan.
sumber
TCO (Tail Call Optimization) adalah proses di mana kompiler pintar dapat melakukan panggilan ke suatu fungsi dan tidak mengambil ruang stack tambahan. Satu- satunya situasi di mana ini terjadi adalah jika instruksi terakhir dieksekusi dalam fungsi f adalah panggilan ke fungsi g (Catatan: g dapat menjadi f ). Kuncinya di sini adalah bahwa f tidak lagi membutuhkan ruang stack - ia hanya memanggil g dan kemudian mengembalikan apa pun yang akan kembali g . Dalam hal ini optimasi dapat dibuat bahwa g hanya berjalan dan mengembalikan nilai apa pun yang diperlukan untuk hal yang disebut f.
Optimasi ini dapat membuat panggilan rekursif mengambil ruang stack konstan, daripada meledak.
Contoh: fungsi faktorial ini tidak TCOptimasi:
Fungsi ini melakukan hal-hal selain memanggil fungsi lain dalam pernyataan pengembaliannya.
Fungsi di bawah ini TCOptimizable:
Ini karena hal terakhir yang terjadi di salah satu fungsi ini adalah memanggil fungsi lain.
sumber
(cons a (foo b))
atau(+ c (bar d))
pada posisi ekor dengan cara yang sama.Mungkin deskripsi tingkat tinggi terbaik yang saya temukan untuk panggilan ekor, panggilan ekor rekursif dan optimasi panggilan ekor adalah posting blog
"Apa-apaan ini: Panggilan ekor"
oleh Dan Sugalski. Pada optimasi panggilan ekor ia menulis:
Dan pada rekursi ekor:
Jadi ini:
diam-diam berubah menjadi:
Apa yang saya sukai dari deskripsi ini adalah betapa ringkas dan mudahnya untuk memahami bagi mereka yang berasal dari latar belakang bahasa imperatif (C, C ++, Java)
sumber
foo
fungsi ekor panggilan awal dioptimalkan? Itu hanya memanggil fungsi sebagai langkah terakhir, dan itu hanya mengembalikan nilai itu, kan?Perhatikan pertama-tama bahwa tidak semua bahasa mendukungnya.
TCO berlaku untuk kasus rekursi khusus. Inti dari itu adalah, jika hal terakhir yang Anda lakukan dalam suatu fungsi adalah memanggil dirinya sendiri (misalnya memanggil dirinya sendiri dari posisi "tail"), ini dapat dioptimalkan oleh kompiler untuk bertindak seperti iterasi alih-alih rekursi standar.
Anda lihat, biasanya selama rekursi, runtime perlu melacak semua panggilan rekursif, sehingga ketika seseorang kembali dapat melanjutkan pada panggilan sebelumnya dan seterusnya. (Coba tuliskan secara manual hasil dari panggilan rekursif untuk mendapatkan ide visual tentang bagaimana ini bekerja.) Melacak semua panggilan membutuhkan ruang, yang menjadi signifikan ketika fungsi panggilan itu sendiri banyak. Tetapi dengan TCO, itu hanya bisa mengatakan "kembali ke awal, hanya saja kali ini mengubah nilai parameter ke yang baru." Itu bisa melakukan itu karena tidak ada setelah panggilan rekursif merujuk pada nilai-nilai itu.
sumber
foo
metode awal panggilan ekor dioptimalkan?Contoh minimal GCC runnable dengan analisis pembongkaran x86
Mari kita lihat bagaimana GCC dapat secara otomatis melakukan optimasi panggilan ekor untuk kita dengan melihat rakitan yang dihasilkan.
Ini akan menjadi contoh yang sangat konkret dari apa yang disebutkan dalam jawaban lain seperti https://stackoverflow.com/a/9814654/895245 bahwa pengoptimalan dapat mengonversi panggilan fungsi rekursif ke loop.
Hal ini pada gilirannya menghemat memori dan meningkatkan kinerja, karena akses memori sering kali merupakan hal utama yang membuat program menjadi lambat saat ini .
Sebagai masukan, kami memberikan GCC faktorial berbasis naive stack yang tidak dioptimalkan:
tail_call.c
GitHub hulu .
Kompilasi dan bongkar:
di mana
-foptimize-sibling-calls
nama generalisasi panggilan ekor menurutman gcc
:seperti yang disebutkan di: Bagaimana cara saya memeriksa apakah gcc melakukan optimasi rekursi ekor?
Saya memilih
-O1
karena:-O0
. Saya menduga bahwa ini karena diperlukan transformasi menengah yang hilang.-O3
menghasilkan kode efisien yang tidak saleh yang tidak akan sangat mendidik, meskipun juga dioptimalkan untuk panggilan ekor.Disassembly dengan
-fno-optimize-sibling-calls
:Dengan
-foptimize-sibling-calls
:Perbedaan utama antara keduanya adalah:
yang
-fno-optimize-sibling-calls
menggunakancallq
, yang merupakan khas non-dioptimalkan fungsi panggilan.Instruksi ini mendorong alamat kembali ke tumpukan, sehingga menambahnya.
Selanjutnya, versi ini juga tidak
push %rbx
, yang mendorong%rbx
ke tumpukan .GCC melakukan ini karena ia menyimpan
edi
, yang merupakan argumen fungsi pertama (n
) keebx
, kemudian panggilanfactorial
.GCC perlu melakukan ini karena sedang mempersiapkan panggilan lain
factorial
, yang akan menggunakan yang baruedi == n-1
.Ia memilih
ebx
karena register ini disimpan dengan sendirinya : Apa register disimpan melalui fungsi panggilan x86-64 linux sehingga subkunci untukfactorial
tidak akan mengubahnya dan kehilangann
.yang
-foptimize-sibling-calls
tidak menggunakan instruksi yang mendorong ke stack: hanya melakukangoto
melompat dalamfactorial
dengan petunjukje
danjne
.Oleh karena itu, versi ini setara dengan loop sementara, tanpa panggilan fungsi apa pun. Penggunaan tumpukan konstan.
Diuji di Ubuntu 18.10, GCC 8.2.
sumber
Lihat di sini:
http://tratt.net/laurie/tech_articles/articles/tail_call_optimization
Seperti yang mungkin Anda ketahui, panggilan fungsi rekursif dapat mendatangkan malapetaka pada tumpukan; mudah kehabisan ruang stack. Optimasi panggilan ekor adalah cara Anda membuat algoritma gaya rekursif yang menggunakan ruang stack konstan, oleh karena itu tidak tumbuh dan tumbuh dan Anda mendapatkan kesalahan stack.
sumber
Kita harus memastikan bahwa tidak ada pernyataan kebagian fungsi itu sendiri .. dijaga dengan panggilan fungsi menjadi hal terakhir dalam fungsi callee.
Rekursi skala besar dapat menggunakan ini untuk optimisasi, tetapi dalam skala kecil, overhead instruksi untuk membuat panggilan fungsi panggilan ekor mengurangi tujuan sebenarnya.
TCO dapat menyebabkan fungsi yang berjalan selamanya:
sumber
Pendekatan fungsi rekursif memiliki masalah. Itu membangun tumpukan panggilan ukuran O (n), yang membuat total biaya memori kami O (n). Ini membuatnya rentan terhadap kesalahan stack overflow, di mana tumpukan panggilan terlalu besar dan kehabisan ruang.
Skema Tail Tail Optimization (TCO). Di mana ia dapat mengoptimalkan fungsi rekursif untuk menghindari penumpukan panggilan yang tinggi dan karenanya menghemat biaya memori.
Ada banyak bahasa yang melakukan TCO seperti (JavaScript, Ruby dan beberapa C) sedangkan Python dan Java tidak melakukan TCO.
Bahasa JavaScript telah dikonfirmasi menggunakan :) http://2ality.com/2015/06/tail-call-optimization.html
sumber
Dalam bahasa fungsional, optimisasi panggilan ekor adalah seolah-olah panggilan fungsi dapat mengembalikan ekspresi yang dievaluasi sebagian sebagai hasilnya, yang kemudian akan dievaluasi oleh pemanggil.
f 6 berkurang menjadi g 6. Jadi jika implementasinya dapat mengembalikan g 6 sebagai hasilnya, dan kemudian memanggil ekspresi itu akan menghemat bingkai tumpukan.
Juga
Mengurangi ke f 6 ke g 6 atau h 6. Jadi jika implementasi mengevaluasi c 6 dan menemukan itu benar maka itu dapat mengurangi,
Penerjemah optimasi panggilan non-ekor sederhana mungkin terlihat seperti ini,
Juru bahasa optimasi panggilan ekor mungkin terlihat seperti ini,
sumber