About ES6 tail call optimization

Time:2021-10-27

ES6 contains a special requirement in the field of performance. This is related to a specific form of optimization involving function calls: tail call optimization (TCO). Simply put, a tail call is a function call that appears at the “end” of another function. After this call, there is nothing else to do (except that the result value may be returned)

What tail call

For example, the following is a non recursive tail call:

function foo(x) {
  return x
}

//Tail call
function bar(y) {
  return foo(y + 1)
}

//Non tail call
function baz() {
  return 1 + bar(40)
}

Baz() // output 42

explain:foo(y+1) yesbar(...)The tail call in becausefoo(...)When finished,bar(...) It’s done, and you just need to returnfoo(...) The result of the call. However,bar(40)It is not a tail call, because after it is completed, its result needs to be added with 1 to be called by thebaz()return.

In JavaScript, calling a new function requires an additional piece of reserved content to manage the call stack and become the stack frame. Therefore, the previous code generally needs to be used for each at the same timebaz()bar(...)foo(...)Keep a stack frame.

However, if the engine supporting TCO can realizefoo(y+1)The call is at the end, which meansbar(...)Basically completed, then callfoo(...)Instead of creating a new frame stack, it can reuse the existing frame stackbar(...)Frame stack. This is not only fast, but also saves memory.

What is tail recursion

In computer science, tail call refers to the case where the last action in a function is a function call: that is, the return value of the call is directly returned by the current function. In this case, the call position is referred to as the tail position. If this function calls itself at the tail position (or other functions of a tail call itself, etc.), this situation is called tail recursion, which is a special case of recursion. Tail calls are not necessarily recursive, but tail recursion is particularly useful and easy to implement.

Significance of TCO

When the program is running, the computer will allocate a certain memory space for the application program; The application program will allocate the obtained memory space by itself, and part of it is used to record the operation of each function being called in the program, which is the function call stack. Conventional function calls always add a new stack frame (stack frame, also translated as “stack frame” or “frame” for short) at the top of the call stack. This process is called “stacking” or “pressing the stack” (that is, pressing the new frame on the top of the stack). When the number of call layers of a function is very large, the call stack will consume a lot of memory, and even burst the memory space (stack overflow), resulting in serious program jam or accidental crash. The call stack of tail call is particularly easy to optimize, which can reduce the use of memory space and improve the running speed. Among them, the optimization effect of tail recursion is the most obvious, especially when the recursion algorithm is very complex.

In simple code snippets, this kind of optimization is nothing, but it solves a big problem when dealing with recursion, especially if recursion may lead to thousands of stack frames. With TCO, the engine can perform all such calls with the same stack frame!

Recursion is a complex topic in JavaScript. Because if there is no TCO, the engine needs to implement an arbitrary limit to define the depth of the recursive stack. When it reaches the limit, it has to stop to prevent memory depletion. With TCO, the recursive function of tail call can essentially run arbitrarily, because there is no need to use additional memory and there is no memory overflow problem.

A typical factorial function is implemented by tail recursion:

//Realized by loop
function factorial(n) {
  if (n<2) return 1

  var res = 1
  for (var i = n; i > 1; i--) {
    res *= i
  }
  return res
}

//Recursive implementation with tail
function factorial(n) {
  function fact(n, res) {
    if (n < 2) return res 
    return fact(n-1, n*res)
  }
  return fact(n, 1)
}

Factorial (5) // output 120

Note: TCO is only used when there are actual tail calls. If you write a function without tail calls, the performance will return to the normal frame stack allocation, and the engine’s restrictions on such recursive call stacks are still valid.

summary

Generally speaking, tail call elimination is optional and can be used or not. However, in functional programming languages, language standards usually require the compiler or running platform to implement tail call elimination. This allows programmers to replace loops with recursion without losing performance. One reason why ES6 requires the engine to implement TCO rather than leaving it to the engine to decide freely is that the lack of TCO will lead to some JavaScript algorithms reducing the probability of recursive implementation because they are afraid of call stack constraints.

If, in all cases, the engine’s lack of TCO only reduces performance, it will not become what ES6 requires. However, because the lack of TCO can make some programs impossible to implement, it has become an important language feature rather than a hidden implementation detail. ES6 ensures that JavaScript developers can rely on this optimization in all ES6 + compliant browsers from now on. This is a victory for JavaScript performance.

reference

This article is composed of blog one article multi posting platformOpenWriterelease!