[Python-ideas] A Continuations Compromise in Python

Steven D'Aprano steve at pearwood.info
Sun May 3 16:34:53 CEST 2009


On Sun, 3 May 2009 10:30:55 pm Antoine Pitrou wrote:
> Steven D'Aprano <steve at ...> writes:
> > It's hardly "premature" to notice that recursive code in Python is
> > significantly slower and less efficient than in other languages.
> > This is a known problem, or at least issue, since some people
> > consider that the solution (tail-recursion optimization) is worse
> > that the problem.
>
> Is there some evidence that this "known issue" has been popping up in
> real-world production Python code (I am not talking about Lisp, C, or
> any other language), rather than academic discussions?
>
> Premature optimization is trying to optimize something which is not a
> significant contributor to execution time.

People spend time trying to speed up general purpose code in the 
standard library, to save 1% or 2% on benchmarks. I'm sure I don't need 
to google for examples -- you've been around the Python-Dev list long 
enough to see plenty of examples, and I for one haven't seen anyone 
objecting to improving Python's general execution speed.

In this case, the speed-up could (in principle) be by up to a factor of 
five or so, not a mere couple of percent. Recursion in Python is quite 
slow compared to iteration. Here's a real (albeit trivial) example: the 
Towers of Hanoi.


def move(n, a, b, c):  # Version using tail recursion.
    "Move n disks from needle a to b using c as temporary storage."
    if n > 0:
        for t in move(n-1, a, c, b):
            yield t
        yield (a, b)
        for t in move(n-1, c, b, a):
            yield t

def move2(n, a, b, c):  # Non-tail recursive version.
    while n > 0:
        for t in move2(n-1, a, c, b):
            yield t
        yield (a, b)
        n -= 1
        a, c = c, a

And in use:

>>> assert list(move(15, 1, 2, 3)) == list(move2(15, 1, 2, 3))
>>> from time import time
>>> t = time(); L = list(move(15, 1, 2, 3)); t = time() - t; print t
0.40623497963
>>> t = time(); L = list(move2(15, 1, 2, 3)); t = time() - t; print t
0.261477947235

Okay, this toy program isn't exactly a mission-critical application, but 
it does demonstrate a genuine performance penalty when using recursion. 
In this case, two recursive calls (one of which is a tail-call) takes 
nearly 60% more time than a single recursive call in a while loop.

But more importantly, execution time is not the only resource that needs 
to be optimized. Programmer effort is an important resource that many 
people wish to optimize. Removing tail-call recursion is a simple 
algorithm quite well suited to be done by the machine, but tedious and 
sometimes tricky for the average programmer to do correctly.

Another is stack space -- hence the relatively low default recursion 
limit. Anyone who has ever seen "maximum recursion depth exceeded" in 
real-world code has a real problem which (if the function is 
tail-recursive) could be fixed by a hypothetical optimizer.



-- 
Steven D'Aprano



More information about the Python-ideas mailing list