Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't tail recursion results in better performance in this code?

I was creating a faster string splitter method. First, I wrote a non-tail recursive version returning List. Next, a tail recursive one using ListBuffer and then calling toList (+= and toList are O(1)). I fully expected the tail recursive version to be faster, but that is not the case.

Can anyone explain why?

Original version:

def split(s: String, c: Char, i: Int = 0): List[String] = if (i < 0) Nil else {
  val p = s indexOf (c, i)
  if (p < 0) s.substring(i) :: Nil else s.substring(i, p) :: split(s, c, p + 1)
}

Tail recursive one:

import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer
def split(s: String, c: Char): Seq[String] = {
  val buffer = ListBuffer.empty[String]
  @tailrec def recurse(i: Int): Seq[String] =  {
    val p = s indexOf (c, i)
    if (p < 0) {
      buffer += s.substring(i)
      buffer.toList
    } else {
      buffer += s.substring(i, p)
      recurse(p + 1)
    }
  }
  recurse(0)
}

This was benchmarked with code here, with results here, by #scala's jyxent.

like image 679
Daniel C. Sobral Avatar asked Jul 28 '11 20:07

Daniel C. Sobral


People also ask

Is tail recursion good for programming?

The tail recursion is better than non-tail recursion. As there is no task left after the recursive call, it will be easier for the compiler to optimize the code. When one function is called, its address is stored inside the stack. So if it is tail recursion, then storing addresses into stack is not needed.

Is tail recursion faster than non-tail?

In simple words, in tail recursion, the recursive function is called last. So it is more efficient than non-tail recursion. Also, the compiler can easily optimize the tail-recursive function, as there isn't any instruction left to be executed because the recursive call is the last statement.

Is recursion bad for performance?

The simplicity of recursion comes at the cost of time and space efficiency. It is much slower than iteration due to the overhead of function calls and control shift from one function to another. It requires extra memory on the stack for each recursive call. This memory gets deallocated when function execution is over.

Why Python doesn't have a tail recursion optimization implement it?

There is no built-in tail recursion optimization in Python. However, we can "rebuild" the function through the Abstract Syntax Tree( AST), eliminating the recursion there and replacing it with a loop.


2 Answers

You're simply doing more work in the second case. In the first case, you might overflow your stack, but every operation is really simple, and :: is as small of a wrapper as you can get (all you have to do is create the wrapper and point it to the head of the other list). In the second case, not only do you create an extra collection initially and have to form a closure around s and buffer for the nested method to use, but you also use the heavierweight ListBuffer which has to check for each += whether it's already been copied out to a list, and uses different code paths depending on whether it's empty or not (in order to get the O(1) append to work).

like image 175
Rex Kerr Avatar answered Nov 15 '22 10:11

Rex Kerr


You expect the tail recursive version to be faster due to the tail call optimization and I think this is right, if you compare apples to apples:

def split3(s: String, c: Char): Seq[String] = {
  @tailrec def recurse(i: Int, acc: List[String] = Nil): Seq[String] =  {
    val p = s indexOf (c, i)
    if (p < 0) {
      s.substring(i) :: acc
    } else {
      recurse(p + 1, s.substring(i, p) :: acc)
    }
  }
  recurse(0) // would need to reverse
}

I timed this split3 to be faster, except of course to get the same result it would need to reverse the result.

It does seem ListBuffer introduces inefficiencies that the tail recursion optimization cannot make up for.

Edit: thinking about avoiding the reverse...

def split3(s: String, c: Char): Seq[String] = {
  @tailrec def recurse(i: Int, acc: List[String] = Nil): Seq[String] =  {
    val p = s lastIndexOf (c, i)
    if (p < 0) {
      s.substring(0, i + 1) :: acc
    } else {
      recurse(p - 1, s.substring(p + 1, i + 1) :: acc)
    }
  }
  recurse(s.length - 1)
}

This has the tail call optimization and avoids ListBuffer.

like image 28
huynhjl Avatar answered Nov 15 '22 10:11

huynhjl