Optimisation de la récursivité de la queue JavaScript

Bonjour lecteur.

Parfois, pour résoudre le problème, vous devez utiliser Recursion , qui a ses avantages et ses inconvénients. J'ai rencontré un problème de débordement de pile.
La profondeur de récursivité maximale est limitée par le moteur JavaScript. Vous pouvez compter avec précision sur 10 000 appels imbriqués, certains interprètes en autorisent davantage, mais pour la plupart d'entre eux, 100 000 appels dépassent le cadre des capacités. Il existe des optimisations automatiques qui aident à éviter de déborder la pile des appels («optimisation de la récursivité de queue»), mais elles ne sont pas encore prises en charge partout et ne fonctionnent que pour les cas simples.

Un exemple de fonction récursive:
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. 


La récursivité de queue permet d'optimiser les appels par le compilateur et est déjà dans la norme ES6 , mais la prise en charge du navigateur laisse beaucoup à désirer .

Un exemple de fonction récursive de queue:
 function sum(number, s = 0){ return number === 0 ? s : sum(number - 1, s + number) } 

Mais, sans prise en charge du navigateur, nous serons confrontés au même problème: le débordement de pile. Nous pouvons essayer d'utiliser avec Trampolining .

Nous allons écrire un décorateur qui prendra une fonction récursive modifiée (étape suivante) et l'exécutera en boucle, sans augmenter la profondeur d'appel.
 function trampoline(fn) { return function(...args) { let result = fn.apply(fn.context, args) while (typeof result === 'function') { result = result() } return result } } 


Maintenant, notre fonction récursive devrait retourner une fonction qui sera immédiatement exécutée par le décorateur. De cette façon, nous créerons conditionnellement un tableau de fonctions et les exécuterons tour à tour. Mais puisque nous renvoyons une nouvelle fonction anonyme à chaque fois, ce code fonctionnera un peu plus lentement.
 function sum(number, s = 0) { return number === 0 ? s : () => sum(number - 1, s + number) } 


Nous utilisons:
 const trampolineSum = trampoline(sum) trampolineSum(100000) // 5000050000 

Ceci est mon premier article. J'ai essayé de ne pas relire les concepts et j'ai fourni un lien vers les sources. Le sujet n'est pas unique et existe depuis longtemps sur les sites anglophones, malheureusement je ne l'ai pas trouvé en russe.

Merci de votre attention. Passez une bonne journée :)

Mettre à jour
Performances:

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


Résultat n ° 1 de Safari 12.1.2
[Debug] run: 0.353ms
[Débogage] exécuté: 0,281 ms
[Debug] run: 0.305ms
[Debug] run: 0.315ms
[Debug] run: 0.319ms
[Débogage] exécuté: 0,231 ms
[Débogage] exécuté: 0,255 ms
[Debug] run: 0.334ms
[Debug] run: 0.370ms
[Débogage] exécuté: 0,274 ms
[Débogage] exécuté: 0,260 ms

Résultat n ° 1 Google Chrome 78.0.3892.0
exécuter: 0.118896484375ms
exécuter: 0.101806640625ms
exécuter: 0.099853515625ms
exécuter: 0.10205078125ms
exécuter: 0.10302734375ms
exécuter: 0.099853515625ms
exécuter: 0.106201171875ms
exécuter: 0.103759765625ms
exécuter: 0.105224609375ms
exécuter: 0,262939453125 ms
exécuter: 0.136962890625ms
exécuter: 0.10107421875ms
exécuter: 0.10302734375ms


# 2 Itération
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") 


Résultat n ° 2 de Safari 12.1.2
[Débogage] exécuté: 0,562 ms
[Debug] run: 0.552ms
[Debug] run: 0.502ms
[Debug] run: 0.527ms
[Debug] run: 0.434ms
[Debug] run: 0.462ms
[Debug] run: 0.511ms
[Débogage] exécuté: 0,528 ms
[Debug] run: 0.549ms
[Debug] run: 0.475ms
[Debug] run: 0.530ms
[Debug] run: 0.494ms

Résultat n ° 2 de Google Chrome 78.0.3892.0
exécuter: 0.932861328125ms
course: 0.751953125ms
exécuter: 0.4580078125ms
exécuter: 0,678955078125 ms
exécuter: 0.424072265625ms
course: 0.505859375ms
exécuter: 0,563720703125ms
exécuter: 0.404052734375ms
exécuter: 0.411865234375ms
exécuter: 0,634033203125ms
exécuter: 0.4169921875ms
exécuter: 0.390869140625ms
exécuter: 0.464111328125ms


# 3 Récursion de la queue et trampoline
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") 


Résultat n ° 3 de Safari 12.1.2
[Debug] run: 0.936ms
[Debug] run: 0.792ms
[Debug] run: 0.882ms
[Debug] run: 0.826ms
[Debug] run: 0.968ms
[Debug] run: 0.818ms
[Debug] run: 1.681ms
[Debug] run: 0.777ms
[Debug] run: 1.109ms
[Debug] run: 0.832ms
[Debug] run: 0.826ms

Résultat n ° 3 de Google Chrome 78.0.3892.0
exécuter: 0.60888671875ms
exécuter: 0.989990234375ms
exécuter: 0,567138671875ms
exécuter: 0.56005859375ms
exécuter: 1.0087890625ms
exécuter: 0.5400390625ms
course: 0,578125 ms
exécuter: 0,541015625 ms
exécuter: 0,557861328125ms
exécuter: 1.97607421875ms
exécuter: 0,570068359375ms
exécuter: 0.593017578125ms
exécuter: 0.530029296875ms
exécuter: 0.89794921875ms
exécuter: 0.590087890625ms


# 4 Récursion de la queue et trampoline (un grand nombre)
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") 


Résultat # 4 de Safari 12.1.2
[Debug] run: 33.693ms
[Debug] run: 24.564ms
[Debug] run: 25.313ms
[Debug] run: 23.262ms
[Debug] run: 24.848ms
[Debug] run: 23.909ms
[Debug] run: 24.248ms
[Debug] run: 32.416ms
[Debug] run: 24.090ms
[Débogage] exécuter: 23,986 ms

Résultat n ° 4 de Google Chrome 78.0.3892.0
course: 40.73681640625ms
course: 33.955078125ms
course: 40.907958984375ms
course: 37.693115234375ms
course: 28.929931640625ms
course: 30.7548828125ms
course: 29.720947265625ms
exécuter: 40.8310546875ms
exécuter: 31.5830078125ms
course: 30.712890625ms
exécuter: 30.162841796875ms
course: 31.56103515625ms


Selon les résultats, nous constatons que notre décorateur, bien qu'il nous permette d'éviter l'erreur de débordement de pile, mais il fonctionne plus lentement que la version récursive et itérative. Cette méthode ne doit donc être utilisée que si vous ne pouvez pas remplacer la récursivité par l'itération ou si vous avez peur des débordements de pile lors de l'exécution de votre fonction récursive.

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


All Articles