Optimierung der JavaScript-Schwanzrekursion

Hallo Leser.

Manchmal müssen Sie zur Lösung des Problems die Rekursion verwenden , die Vor- und Nachteile hat. Ich bin auf ein Stapelüberlaufproblem gestoßen.
Die maximale Rekursionstiefe wird durch die JavaScript-Engine begrenzt. Sie können genau mit 10.000 verschachtelten Anrufen rechnen, einige Dolmetscher erlauben mehr, aber für die meisten von ihnen liegen 100.000 Anrufe außerhalb des Funktionsumfangs. Es gibt automatische Optimierungen, die helfen, ein Überlaufen des Aufrufstapels zu vermeiden ("Tail-Rekursionsoptimierung"), aber sie werden noch nicht überall unterstützt und funktionieren nur in einfachen Fällen.

Ein Beispiel für eine rekursive Funktion:
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. 


Die Schwanzrekursion ermöglicht die Optimierung von Aufrufen durch den Compiler und ist bereits im ES6- Standard enthalten, aber die Browserunterstützung lässt zu wünschen übrig .

Ein Beispiel für eine rekursive Schwanzfunktion:
 function sum(number, s = 0){ return number === 0 ? s : sum(number - 1, s + number) } 

Ohne Browserunterstützung treten jedoch dieselben Probleme auf - der Stapelüberlauf. Wir können versuchen, zusammen mit Trampolin zu verwenden .

Wir werden einen Dekorator schreiben, der eine modifizierte rekursive Funktion (nächster Schritt) übernimmt und in einer Schleife ausführt, ohne die Aufruftiefe zu erhöhen.
 function trampoline(fn) { return function(...args) { let result = fn.apply(fn.context, args) while (typeof result === 'function') { result = result() } return result } } 


Jetzt sollte unsere rekursive Funktion eine Funktion zurückgeben, die sofort vom Dekorateur ausgeführt wird. Auf diese Weise erstellen wir bedingt ein Array von Funktionen und führen sie nacheinander aus. Da wir jedoch jedes Mal eine neue anonyme Funktion zurückgeben, arbeitet dieser Code etwas langsamer.
 function sum(number, s = 0) { return number === 0 ? s : () => sum(number - 1, s + number) } 


Wir verwenden:
 const trampolineSum = trampoline(sum) trampolineSum(100000) // 5000050000 

Dies ist mein erster Artikel. Ich habe versucht, die Konzepte nicht erneut zu erzählen, und einen Link zu den Quellen bereitgestellt. Das Thema ist nicht eindeutig und existiert seit langem auf englischsprachigen Websites. Leider habe ich es nicht auf Russisch gefunden.

Vielen Dank für Ihre Aufmerksamkeit. Einen schönen Tag noch :)

Update
Leistung:

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


Ergebnis Nr. 1 von Safari 12.1.2
[Debug] ausführen: 0,353 ms
[Debug] ausführen: 0,281 ms
[Debug] -Lauf: 0,305 ms
[Debug] -Lauf: 0,315 ms
[Debug] ausführen: 0,319 ms
[Debug] ausführen: 0,231 ms
[Debug] -Lauf: 0,255 ms
[Debug] ausführen: 0,334 ms
[Debug] ausführen: 0,370 ms
[Debug] ausführen: 0,274 ms
[Debug] -Lauf: 0,260 ms

Ergebnis Nr. 1 Google Chrome 78.0.3892.0
Lauf: 0,118896484375ms
Lauf: 0,101806640625ms
Lauf: 0,099853515625 ms
Lauf: 0,10205078125 ms
Lauf: 0,10302734375ms
Lauf: 0,099853515625 ms
Lauf: 0,106201171875ms
Lauf: 0,103759765625ms
Lauf: 0,105224609375ms
Lauf: 0,262939453125 ms
Lauf: 0,136962890625ms
Lauf: 0,10107421875ms
Lauf: 0,10302734375ms


# 2 Iteration
Code # 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") 


Ergebnis Nr. 2 von Safari 12.1.2
[Debug] -Lauf: 0,562 ms
[Debug] -Lauf: 0,552 ms
[Debug] ausführen: 0,502 ms
[Debug] -Lauf: 0,527 ms
[Debug] -Lauf: 0,434 ms
[Debug] ausführen: 0,462 ms
[Debug] -Lauf: 0,511 ms
[Debug] -Lauf: 0,528 ms
[Debug] -Lauf: 0,549 ms
[Debug] -Lauf: 0,475 ms
[Debug] -Lauf: 0,530 ms
[Debug] -Lauf: 0,494 ms

Ergebnis Nr. 2 von Google Chrome 78.0.3892.0
Lauf: 0,932861328125ms
Lauf: 0,751953125 ms
Lauf: 0,4580078125 ms
Lauf: 0,678955078125 ms
Lauf: 0,424072265625 ms
Lauf: 0,505859375ms
Lauf: 0,563720703125 ms
Lauf: 0,404052734375ms
Lauf: 0,411865234375ms
Lauf: 0,634033203125 ms
Lauf: 0,4169921875 ms
Lauf: 0,390869140625ms
Lauf: 0,464111328125ms


# 3 Schwanzrekursion und Trampolin
Code # 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") 


Ergebnis Nr. 3 von Safari 12.1.2
[Debug] ausführen: 0,936 ms
[Debug] ausführen: 0,792 ms
[Debug] ausführen: 0,882 ms
[Debug] ausführen: 0,826 ms
[Debug] ausführen: 0,968 ms
[Debug] -Lauf: 0,818 ms
[Debug] ausführen: 1.681ms
[Debug] ausführen: 0,777 ms
[Debug] ausführen: 1.109ms
[Debug] ausführen: 0,832 ms
[Debug] ausführen: 0,826 ms

Ergebnis Nr. 3 von Google Chrome 78.0.3892.0
Lauf: 0,60888671875 ms
Lauf: 0,989990234375ms
Lauf: 0,567138671875 ms
Lauf: 0,56005859375ms
Lauf: 1.0087890625ms
Lauf: 0,5400390625 ms
Lauf: 0,578125 ms
Lauf: 0,541015625 ms
Lauf: 0,557861328125ms
Lauf: 1.97607421875ms
Lauf: 0,570068359375ms
Lauf: 0,593017578125 ms
Lauf: 0,530029296875ms
Lauf: 0,89794921875ms
Lauf: 0,590087890625 ms


# 4 Schwanzrekursion und Trampolin (eine große Anzahl)
Code # 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") 


Ergebnis Nr. 4 von Safari 12.1.2
[Debug] ausführen: 33.693ms
[Debug] ausführen: 24.564ms
[Debug] ausführen: 25.313ms
[Debug] ausführen: 23.262ms
[Debug] ausführen: 24.848ms
[Debug] ausführen: 23.909ms
[Debug] ausführen: 24.248ms
[Debug] ausführen: 32.416ms
[Debug] ausführen: 24.090ms
[Debug] ausführen: 23.986ms

Ergebnis Nr. 4 von Google Chrome 78.0.3892.0
Lauf: 40.73681640625ms
Lauf: 33,955078125 ms
Lauf: 40.907958984375ms
run: 37.693115234375ms
Lauf: 28.929931640625ms
Lauf: 30.7548828125ms
Lauf: 29.720947265625ms
Lauf: 40.8310546875ms
Lauf: 31.5830078125ms
Lauf: 30.712890625ms
Lauf: 30.162841796875ms
Lauf: 31.56103515625ms


Den Ergebnissen zufolge sehen wir, dass unser Dekorateur zwar den Stapelüberlauffehler vermeiden kann, aber langsamer arbeitet als die rekursive und iterative Version. Daher sollte diese Methode nur verwendet werden, wenn Sie die Rekursion nicht durch Iteration ersetzen können oder Angst vor Stapelüberläufen haben, wenn Sie Ihre rekursive Funktion ausführen.

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


All Articles