Optimización de recursión de cola de JavaScript

Hola lector

A veces, para resolver el problema, debe usar Recursion , que tiene sus ventajas y desventajas. Me encontré con un problema de desbordamiento de pila.
La profundidad máxima de recursión está limitada por el motor de JavaScript. Puede contar con precisión con 10,000 llamadas anidadas, algunos intérpretes permiten más, pero para la mayoría de ellas 100,000 llamadas están fuera del alcance de las capacidades. Existen optimizaciones automáticas que ayudan a evitar el desbordamiento de la pila de llamadas ("optimización de recursión de cola"), pero aún no son compatibles en todas partes y funcionan solo para casos simples.

Un ejemplo de una función recursiva:
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 recursividad de cola permite optimizar las llamadas del compilador y ya está en el estándar ES6 , pero el soporte del navegador deja mucho que desear .

Un ejemplo de una función recursiva de cola:
 function sum(number, s = 0){ return number === 0 ? s : sum(number - 1, s + number) } 

Pero, sin el soporte del navegador, enfrentaremos el mismo problema: desbordamiento de pila. Podemos intentar usarlo junto con Trampolining .

Escribiremos un decorador que acepte la función recursiva modificada (siguiente paso) y la ejecute en un bucle, sin aumentar la profundidad de la llamada.
 function trampoline(fn) { return function(...args) { let result = fn.apply(fn.context, args) while (typeof result === 'function') { result = result() } return result } } 


Ahora nuestra función recursiva debería devolver una función que será ejecutada inmediatamente por el decorador. De esta manera, condicionalmente haremos una serie de funciones y las ejecutaremos a su vez. Pero dado que devolvemos una nueva función anónima cada vez, este código funcionará un poco más lento.
 function sum(number, s = 0) { return number === 0 ? s : () => sum(number - 1, s + number) } 


Utilizamos:
 const trampolineSum = trampoline(sum) trampolineSum(100000) // 5000050000 

Este es mi primer artículo. Intenté no volver a contar los conceptos y proporcioné un enlace a las fuentes. El tema no es único y ha existido durante mucho tiempo en sitios en inglés, desafortunadamente no lo encontré en ruso.

Gracias por su atencion Que tengas un buen día :)

Actualización
Rendimiento:

# 1 recursividad
Código # 1
 function sum(n) { return n === 0 ? 0 : n + sum(n - 1) } console.time("run") sum(1000) console.timeEnd("run") 


Resultado # 1 de Safari 12.1.2
[Depurar] ejecutar: 0.353 ms
[Depurar] ejecutar: 0.281 ms
[Debug] run: 0.305ms
[Debug] run: 0.315ms
[Debug] run: 0.319ms
[Depurar] ejecutar: 0.231 ms
[Depurar] ejecutar: 0.255 ms
[Depurar] ejecutar: 0.334 ms
[Debug] run: 0.370ms
[Depurar] ejecutar: 0.274 ms
[Depurar] ejecutar: 0.260ms

Resultado # 1 Google Chrome 78.0.3892.0
ejecutar: 0.118896484375ms
ejecutar: 0.101806640625ms
ejecutar: 0.099853515625ms
ejecutar: 0.10205078125ms
ejecutar: 0.10302734375ms
ejecutar: 0.099853515625ms
ejecutar: 0.106201171875ms
ejecutar: 0.103759765625ms
ejecutar: 0.105224609375ms
ejecutar: 0.262939453125ms
ejecutar: 0.136962890625ms
ejecutar: 0.10107421875ms
ejecutar: 0.10302734375ms


# 2 iteración
Código # 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") 


Resultado # 2 de Safari 12.1.2
[Debug] run: 0.562ms
[Debug] run: 0.552ms
[Debug] run: 0.502ms
[Debug] run: 0.527ms
[Debug] run: 0.434ms
[Debug] run: 0.462ms
[Debug] run: 0.511ms
[Debug] run: 0.528ms
[Debug] run: 0.549ms
[Debug] run: 0.475ms
[Debug] run: 0.530ms
[Debug] run: 0.494ms

Resultado # 2 de Google Chrome 78.0.3892.0
ejecutar: 0.932861328125ms
ejecutar: 0.751953125ms
ejecutar: 0.4580078125ms
ejecutar: 0.678955078125ms
ejecutar: 0.424072265625ms
ejecutar: 0.505859375ms
ejecutar: 0.563720703125ms
ejecutar: 0.404052734375ms
ejecutar: 0.411865234375ms
ejecutar: 0.634033203125ms
ejecutar: 0.4169921875ms
ejecutar: 0.390869140625ms
ejecutar: 0.464111328125ms


# 3 Recurrencia de cola y trampolín
Código # 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") 


Resultado # 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

Resultado # 3 de Google Chrome 78.0.3892.0
ejecutar: 0.60888671875ms
ejecutar: 0.989990234375ms
ejecutar: 0.567138671875ms
ejecutar: 0.56005859375ms
ejecutar: 1.0087890625ms
ejecutar: 0.5400390625ms
ejecutar: 0.578125ms
ejecutar: 0.541015625ms
ejecutar: 0.557861328125ms
ejecutar: 1.97607421875ms
ejecutar: 0.570068359375ms
ejecutar: 0.593017578125ms
ejecutar: 0.530029296875ms
ejecutar: 0.89794921875ms
ejecutar: 0.590087890625ms


# 4 Recurrencia de cola y trampolín (un gran número)
Código # 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") 


Resultado # 4 de Safari 12.1.2
[Depurar] ejecutar: 33.693 ms
[Depurar] ejecutar: 24.564 ms
[Debug] run: 25.313ms
[Debug] run: 23.262ms
[Depurar] ejecutar: 24.848 ms
[Debug] run: 23.909ms
[Depurar] ejecutar: 24.248 ms
[Debug] run: 32.416ms
[Debug] run: 24.090ms
[Debug] run: 23.986ms

Resultado # 4 de Google Chrome 78.0.3892.0
ejecutar: 40.73681640625ms
ejecutar: 33.955078125ms
ejecutar: 40.907958984375ms
ejecutar: 37.693115234375ms
ejecutar: 28.929931640625ms
ejecutar: 30.7548828125ms
ejecutar: 29.720947265625ms
ejecutar: 40.8310546875ms
ejecutar: 31.5830078125ms
ejecutar: 30.712890625ms
ejecutar: 30.162841796875ms
ejecutar: 31.56103515625ms


Según los resultados, vemos que nuestro decorador, aunque nos permite evitar el error de desbordamiento de la pila, funciona más lentamente que la versión recursiva e iterativa. Por lo tanto, este método debe usarse solo si no puede reemplazar la recursividad con la iteración, o si tiene miedo del desbordamiento de la pila al ejecutar su función recursiva.

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


All Articles