Optimisasi rekursi ekor JavaScript

Halo pembaca.

Kadang-kadang untuk menyelesaikan masalah Anda harus menggunakan Rekursi , yang memiliki pro dan kontra. Saya mengalami masalah stack overflow.
Kedalaman rekursi maksimum dibatasi oleh mesin JavaScript. Anda dapat menghitung secara akurat 10.000 panggilan bersarang, beberapa penerjemah mengizinkan lebih, tetapi untuk sebagian besar dari mereka 100.000 panggilan berada di luar jangkauan kemampuan. Ada optimasi otomatis yang membantu menghindari meluapnya tumpukan panggilan ("optimisasi rekursi ekor"), tetapi belum didukung di mana-mana dan hanya berfungsi untuk kasus sederhana.

Contoh fungsi rekursif:
function sum(n) { return n === 0 ? 0 : n + sum(n - 1) } sum(5) // 1 + 2 + 3 + 4 + 5 = 15 sum(100000) // Error: Maximum call stack size exceeded. 


Tail recursion memungkinkan optimalisasi panggilan oleh kompiler dan sudah dalam standar ES6 , tetapi dukungan browser menyisakan banyak yang diinginkan .

Contoh fungsi rekursif ekor:
 function sum(number, s = 0){ return number === 0 ? s : sum(number - 1, s + number) } 

Tapi, tanpa dukungan browser, kita akan menghadapi masalah yang sama - stack overflow. Kita bisa mencoba menggunakannya bersama dengan Trampolining .

Kami akan menulis dekorator yang akan mengambil fungsi rekursif yang dimodifikasi (langkah berikutnya) dan menjalankannya dalam satu lingkaran, tanpa meningkatkan kedalaman panggilan.
 function trampoline(fn) { return function(...args) { let result = fn.apply(fn.context, args) while (typeof result === 'function') { result = result() } return result } } 


Sekarang fungsi rekursif kami harus mengembalikan fungsi yang akan segera dieksekusi oleh dekorator. Dengan cara ini, kita secara kondisional akan membuat berbagai fungsi dan menjalankannya secara bergantian. Tetapi karena kita mengembalikan fungsi anonim baru setiap kali, kode ini akan bekerja sedikit lebih lambat.
 function sum(number, s = 0) { return number === 0 ? s : () => sum(number - 1, s + number) } 


Kami menggunakan:
 const trampolineSum = trampoline(sum) trampolineSum(100000) // 5000050000 

Ini artikel pertama saya. Saya mencoba untuk tidak menceritakan kembali konsep dan memberikan tautan ke sumber. Topiknya tidak unik dan sudah lama ada di situs berbahasa Inggris, sayangnya saya tidak menemukannya dalam bahasa Rusia.

Terima kasih atas perhatian anda Semoga harimu menyenangkan :)

Perbarui
Kinerja:

Rekursi # 1
Kode # 1
 function sum(n) { return n === 0 ? 0 : n + sum(n - 1) } console.time("run") sum(1000) console.timeEnd("run") 


Hasil # 1 dari Safari 12.1.2
[Debug] jalankan: 0.353ms
[Debug] jalankan: 0.281ms
[Debug] jalankan: 0.305ms
[Debug] jalankan: 0.315ms
[Debug] jalankan: 0.319ms
[Debug] jalankan: 0,231 ms
[Debug] jalankan: 0.255ms
[Debug] jalankan: 0.334ms
[Debug] jalankan: 0,370 ms
[Debug] jalankan: 0.274ms
[Debug] jalankan: 0,260 ms

Hasil # 1 Google Chrome 78.0.3892.0
jalankan: 0.118896484375ms
jalankan: 0.101806640625ms
jalankan: 0,099853515625ms
jalankan: 0.10205078125ms
jalankan: 0.10302734375ms
jalankan: 0,099853515625ms
jalankan: 0.106201171875ms
jalankan: 0.103759765625ms
jalankan: 0.105224609375ms
jalankan: 0,262939453125ms
jalankan: 0.136962890625ms
jalankan: 0.10107421875ms
jalankan: 0.10302734375ms


Iterasi # 2
Kode # 2
 function sum(n) { let sum = 0 for (let i = 1; i <= n; i++) { sum += i } return sum } console.time("run") sum(1000) console.timeEnd("run") 


Hasil # 2 dari Safari 12.1.2
[Debug] jalankan: 0,562ms
[Debug] jalankan: 0,552ms
[Debug] jalankan: 0,502ms
[Debug] jalankan: 0,527ms
[Debug] jalankan: 0.434ms
[Debug] jalankan: 0,462 ms
[Debug] jalankan: 0,511ms
[Debug] jalankan: 0,528ms
[Debug] jalankan: 0,549ms
[Debug] jalankan: 0,475 ms
[Debug] jalankan: 0,530 ms
[Debug] jalankan: 0.494ms

Hasil # 2 dari Google Chrome 78.0.3892.0
jalankan: 0.932861328125ms
jalankan: 0.751953125ms
jalankan: 0.4580078125ms
jalankan: 0.678955078125ms
jalankan: 0.424072265625ms
jalankan: 0,505859375ms
jalankan: 0,563720703125ms
jalankan: 0.404052734375ms
jalankan: 0.411865234375ms
jalankan: 0.634033203125ms
jalankan: 0.4169921875ms
jalankan: 0.390869140625ms
jalankan: 0.464111328125ms


# 3 Ekor rekursi dan trampolin
Kode # 3
 function trampoline(fn) { return function(...args) { let result = fn.apply(fn.context, args) while (typeof result === 'function') { result = result() } return result } } function sum(number, s = 0) { return number === 0 ? s : () => sum(number - 1, s + number) } const trampolineSum = trampoline(sum) console.time("run") trampolineSum(1000) console.timeEnd("run") 


Hasil # 3 dari Safari 12.1.2
[Debug] jalankan: 0,936 ms
[Debug] jalankan: 0.792ms
[Debug] jalankan: 0.882ms
[Debug] jalankan: 0.826ms
[Debug] jalankan: 0,968 ms
[Debug] jalankan: 0,818 ms
[Debug] jalankan: 1.681ms
[Debug] jalankan: 0.777ms
[Debug] jalankan: 1.109ms
[Debug] jalankan: 0.832ms
[Debug] jalankan: 0.826ms

Hasil # 3 dari Google Chrome 78.0.3892.0
jalankan: 0.60888671875ms
jalankan: 0.989990234375ms
jalankan: 0,567138671875ms
jalankan: 0,56005859375ms
jalankan: 1,0087890625ms
jalankan: 0,5400390625ms
jalankan: 0,578125ms
jalankan: 0,541015625ms
jalankan: 0,557861328125ms
jalankan: 1.97607421875ms
jalankan: 0,570068359375ms
jalankan: 0,593017578125ms
jalankan: 0,530029296875ms
jalankan: 0.89794921875ms
jalankan: 0,590087890625ms


# 4 Ekor rekursi dan trampolin (dalam jumlah besar)
Kode # 4
 function trampoline(fn) { return function(...args) { let result = fn.apply(fn.context, args) while (typeof result === 'function') { result = result() } return result } } function sum(number, s = 0) { return number === 0 ? s : () => sum(number - 1, s + number) } const trampolineSum = trampoline(sum) console.time("run") trampolineSum(100000) console.timeEnd("run") 


Hasil # 4 dari Safari 12.1.2
[Debug] jalankan: 33.693ms
[Debug] jalankan: 24.564 ms
[Debug] jalankan: 25.313 ms
[Debug] jalankan: 23.262 ms
[Debug] jalankan: 24.848 ms
[Debug] jalankan: 23,909 ms
[Debug] jalankan: 24.248 ms
[Debug] jalankan: 32,416ms
[Debug] jalankan: 24.090 ms
[Debug] jalankan: 23.986 ms

Hasil # 4 dari Google Chrome 78.0.3892.0
jalankan: 40.73681640625ms
jalankan: 33.955078125ms
jalankan: 40.907958984375ms
jalankan: 37.693115234375ms
jalankan: 28.929931640625ms
jalankan: 30.7548828125ms
jalankan: 29.720947265625ms
jalankan: 40.8310546875ms
jalankan: 31.5830078125ms
jalankan: 30.712890625ms
jalankan: 30.162841796875ms
jalankan: 31.56103515625ms


Menurut hasil, kami melihat bahwa dekorator kami, meskipun memungkinkan kami untuk menghindari kesalahan stack overflow, tetapi bekerja lebih lambat daripada versi rekursif dan berulang. Jadi metode ini harus digunakan hanya jika Anda tidak dapat mengganti rekursi dengan iterasi atau takut tumpukan berlebih saat menjalankan fungsi rekursif Anda.

Source: https://habr.com/ru/post/id464915/


All Articles