11

I have written a verilog (logic gates and their connectivity description basically) simulator in python as a part of an experiment.

I faced an issue with the stack limit so I did some reading and found that Python does not have a "tail call optimization" feature (i.e. removing stack entries dynamically as recursion proceeds)

I mainly have two questions in this regard:

1) If I bump up the stack limit to sys.setrecursionlimit(15000) does it impact performance in terms of time (memory -- I do not care)?

2) Is there any way I can circumvent this limitation assuming that I can live without a stack-trace.
I ask this because Verilog mainly deals with state-machines which can be implemented in an elegant way using recursive functions.

Also, if I may add, in case of recursive function calls, if there is a bug, I rely more on the input which is causing this bug rather than the stack trace.

I am new to Python, so maybe experts might argue that the Python stack trace is quite useful to debug recursive function calls...if that is the case, I would be more than happy to learn how to do that.

Lastly, is it advisable to write recursive functions in Python or should I be moving to other languages?

If there any work-around such that I can continue using python for recursive functions, I would like to know if there any performance impact (I can do profiling though).

sanjay
  • 647
  • 1
  • 6
  • 14
  • 1
    I do not know how it affects performance in theory, but I have never had major issues and have always set the `sys.setrecursionlimit` to its maximum possible value because I have had issues with large classes/functions hitting the default maximum recursion depth. That said, as the answers below explain, most of the time there is a faster way to do whatever it is you are doing other than a long series of recursive calls. – Michael Aug 13 '14 at 23:55
  • 2
    If you can do tail call optimization, then you don't need a stack to simulate the recursion. It'll be trivial to convert it to just use regular stackless loop. – Lie Ryan Aug 14 '14 at 00:57

7 Answers7

6

2) Is there any way I can circumvent this limitation assuming that I can live without a stack-trace. I ask this because Verilog mainly deals with state-machines which can be implemented in an elegant way using recursive functions.

There is a way to avoid tail calls without changing your existing logic too much, simply rewrite your tail calls to return a thunk and use a trampoline to call that thunk. If you need to pass in complex state between transition, you can use continuation passing style to pass them around. This style of writing code is very well suited for writing a state machine.

An example is perhaps clearer, suppose that you start with this recursive implementation of a fizzbuzz state machine that uses tail calls to pass control to the next transition:

def start():
    return increment(0)

def fizz(n):
    print 'fizz'
    return increment(n)

def buzz(n):
    print 'buzz'
    return increment(n)

def fizzbuzz(n):
    print 'fizzbuzz'
    return increment(n)

def increment(n):
    n = n + 1
    if n > 100:
        return terminate()
    elif n % 3 == 0 and n % 5 == 0: 
        return fizzbuzz(n)
    elif n % 3 == 0: 
        return fizz(n)
    elif n % 5 == 0:
        return buzz(n)
    else:
        print n
        return increment(n)

def terminate():
    raise StopIteration

try:
    start()
except StopIteration:
    pass

To avoid the tail calls, you simply wrap all the tail calls in lambda (or alternatively, functools.partial) and add a trampoline:

def start():
    return lambda: increment(0)

def fizz(n):
    print 'fizz'
    return lambda: increment(n)

def buzz(n):
    print 'buzz'
    return lambda: increment(n)

def fizzbuzz(n):
    print 'fizzbuzz'
    return lambda: increment(n)

def increment(n):
    n = n + 1
    if n > 2000:
        # strictly speaking, transitions that takes no arguments
        # like terminate don't need to be wrapped in lambda
        # this is added here only for symmetry with others
        return lambda: terminate()
    elif n % 3 == 0 and n % 5 == 0: 
        return lambda: fizzbuzz(n)
    elif n % 3 == 0: 
        return lambda: fizz(n)
    elif n % 5 == 0:
        return lambda: buzz(n)
    else:
        print n
        return lambda: increment(n)

def terminate():
    raise StopIteration

def trampoline(func): 
    try:
        while True:
            func = func()
    except StopIteration:
        pass

trampoline(lambda: start())

Now you can have lots more fizzbuzz without hitting the recursion limit.

Lie Ryan
  • 62,238
  • 13
  • 100
  • 144
  • 1
    Thanks a lot for the pointer!! This solved my problem with the recursive functions but I have anyway moved to a non-recursive way of solving my problem...Recursive solution was slow – sanjay Aug 22 '14 at 19:23
  • Thank you for introducing me to a new (for me) way of programming! I am not sure the pattern is quite stuck in my mind yet, but this would be enjoyable to try out at some point. Using thunks and trampolines is a really neat idea and can (obviously) be used to implement some rather interesting algorithms. Do you write the first and then the conversion? – Noctis Skytower Jun 25 '15 at 17:24
3

A lot depends on the specific nature of the recursive solution you're trying to implement. Let me give a concrete example. Suppose you want the sum of all values in a list. You can set the recursion up by adding the first value to the sum of the remainder of the list - the recursion should be obvious. However, the recursive subproblem is only 1 smaller than the original problem, so the recursive stack will grow to be as big as the number of items in the list. For large lists this will be a problem. An alternate recursion is to note that the sum of all values is the sum of the first half of the list plus the sum of the second half of the list. Again, the recursion should be obvious and the terminating condition is when you get down to sublists of length 1. However, for this version the stack will only grow as log2 of the size of the list, and you can handle immense lists without stack problems. Not all problems can be factored into subproblems which are half the size, but when you can this is a good way to avoid stack overflow situations.

If your recursive solution is a tail recursion, you can easily be converted into a loop rather than a recursive call.

Another possibility if you don't have tail recursion is to implement things with a loop and explicitly store your intermediate state on an explicit stack.

pjs
  • 18,696
  • 4
  • 27
  • 56
  • So you are suggesting a fork-join method... What you are suggesting would mean that I have to restructure my program (significantly)...my main motivation behind asking this question is to see if I can choose to have some kind of mechanism wherein I can run my program with a stack limit of 1000 (say) even when it requires 10k+ PS: I'm actually editing my code to have several independent lists to solve in parallel. – sanjay Aug 13 '14 at 23:13
3

See Does Python optimize tail recursion?

Guido Van Rossum says that using a lot of recursion is "simply unpythonic" : http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html

But many people have tried to hack up their own support anyway. E.g. http://tomforb.es/adding-tail-call-optimization-to-python . Or just google "python tail call"

Community
  • 1
  • 1
ykaganovich
  • 14,736
  • 8
  • 59
  • 96
  • That's a good implementation. Unfortunately the google results I see are fairly broken, not even supporting mutual recursion. – Andrew Johnson Aug 14 '14 at 00:17
2

Note: This answer is limited to your topmost question, i.e. "Is it advisable to write recursive functions in Python?".

The short answer is no, it's not exactly "advisable". Without tail-call optimization, recursion can get painfully slow in Python given how intensive function calls are on both memory and processor time. Whenever possible, it's best to rewrite your code iteratively.

dpwilson
  • 997
  • 9
  • 19
2

Addressing specifically your question marked 1), changing the recursion limit is dangerous in that it may allow an overflow of the underlying C stack. See also this question: What is the maximum recursion depth in Python, and how to increase it?

Community
  • 1
  • 1
Greg Ball
  • 3,671
  • 3
  • 22
  • 15
0

I use sys.setrecursionlimit to set the recursion limit to its maximum possible value because I have had issues with large classes/functions hitting the default maximum recursion depth. Setting a large value for the recursion limit should not affect the performance of your script, i.e. it will take the same amount of time to complete if it completes under both a high and a low recursion limit. The only difference is that if you have a low recursion limit, it prevents you from doing stupid things (like running an infinitely recursive loop). With a high limit, rather than hit the limit, a horribly inefficient script that uses recursion too much will just run forever (or until it runs out of memory depending on the task).

As the other answers explain in more detail, most of the time there is a faster way to do whatever it is that you are doing other than a long series of recursive calls.

Michael
  • 13,244
  • 23
  • 67
  • 115
-2

I have seen decorators trying to implement tail-recursion in python, so I took a stab at it myself. Here is a pure python (no sys._getframe) implementation of tail-recursion optimization that allows for mutual recursion.

class TailRecurseException(Exception):
    def __init__(self, func, args, kwargs):
        self.func = func
        self.args = args
        self.kwargs = kwargs

def tail_recursive(g, rec=[]):
    def func(*args, **kwargs):
        if g in rec:
            raise TailRecurseException(g, args, kwargs)
        rec.append( g )
        while True:
           try:
               r = g(*args, **kwargs)
               rec.remove( g )
               return r
           except TailRecurseException, e:
               if e.func==g:
                   args = e.args
                   kwargs = e.kwargs
               else:
                   rec.remove( g )
                   raise e
    return func

@tail_recursive
def g(n):
    if n==0:
        return 0
    else:
        return f(n-1)

@tail_recursive
def f(n):
    if n == 0:
        return 0
    else:
        return g(n-1)

print f(100000)
Andrew Johnson
  • 3,078
  • 1
  • 18
  • 24