为什么尾递归优化比Python中的正常递归更快?

时间:2016-05-12 16:54:31

标签: python performance recursion tail-recursion benchmarking

虽然我知道尾部递归优化是非Pythonic的,但我想到了一个快速破解这里的问题,一旦我准备发布就删除了。

对于1000个堆栈限制,深度递归算法在Python中不可用。但有时通过解决方案对初步想法很有帮助。由于函数是Python中的第一类,我使用返回有效函数和下一个值。然后循环调用该进程,直到完成单个调用。我确定这不是新的。

我发现有趣的是我期望来回传递函数的额外开销使得这比正常递归慢。在我的粗略测试期间,我发现它需要30-50%的正常递归时间。 (允许LONG递归的额外好处。)

以下是我正在运行的代码:

from contextlib import contextmanager
import time

# Timing code from StackOverflow most likely.
@contextmanager
def time_block(label):
    start = time.clock()
    try:
        yield
    finally:
        end = time.clock()
        print ('{} : {}'.format(label, end - start))


# Purely Recursive Function
def find_zero(num):
    if num == 0:
        return num
    return find_zero(num - 1)


# Function that returns tuple of [method], [call value]
def find_zero_tail(num):
    if num == 0:
        return None, num
    return find_zero_tail, num - 1


# Iterative recurser
def tail_optimize(method, val):
    while method:
        method, val = method(val)
    return val


with time_block('Pure recursion: 998'):
    find_zero(998)

with time_block('Tail Optimize Hack: 998'):
    tail_optimize(find_zero_tail, 998)

with time_block('Tail Optimize Hack: 1000000'):
    tail_optimize(find_zero_tail, 10000000)

# One Run Result:
# Pure recursion: 998 : 0.000372791020758
# Tail Optimize Hack: 998 : 0.000163852100569
# Tail Optimize Hack: 1000000 : 1.51006975627

为什么第二种风格更快?

我的猜测是在堆栈上创建条目的开销,但我不知道如何查找。

编辑:

在玩通话计数时,我做了一个循环来尝试两种不同的num值。当我循环并多次调用时,递归更接近奇偶校验。

所以,我在时间之前添加了这个,这是一个新名称的find_zero:

def unrelated_recursion(num):
    if num == 0:
        return num
    return unrelated_recursion(num - 1)

unrelated_recursion(998)

现在尾部优化调用是完全递归的85%。

所以我的理论是,相对于单个堆栈,15%的惩罚是较大堆栈的开销。

我在每次只运行一次时看到如此巨大的执行时间差异的原因是分配堆栈内存和结构的代价。一旦分配,使用它们的成本就会大大降低。

因为我的算法很简单,所以内存结构分配是执行时间的很大一部分。

当我将堆栈启动调用切换到unrelated_recursion(499)时,我在find_zero(998)执行时间内完全启动和未启动堆栈之间的大约一半。这对理论很有意义。

1 个答案:

答案 0 :(得分:3)

作为评论有希望让我回忆,我并没有真正回答这个问题,所以这是我的观点:

在优化中,你要分配,解包和解除分配元组,所以我尝试了没有它们:

# Function that returns tuple of [method], [call value]
def find_zero_tail(num):
    if num == 0:
        return None
    return num - 1


# Iterative recurser
def tail_optimize(method, val):
    while val:
        val = method(val)
    return val

1000次尝试,每次尝试以值= 998开始:

  • 此版本需要0.16秒
  • 你的"优化"版本耗时0.22秒
  • "未经优化的"一个拿了0.29s

(请注意,对我来说,您的优化版本比未经优化的版本更快......但我们不会进行完全相同的测试。)

但我不认为这对于获取这些统计数据是有用的:成本更多地放在Python(方法调用,元组分配......)上,使代码能够完成实际操作。在实际应用中,您最终不会测量1000个元组的成本,而是衡量实际实现的成本。

但是根本不这样做:这几乎没有什么难以阅读,你为读者写作,而不是为机器写作:

# Function that returns tuple of [method], [call value]
def find_zero_tail(num):
    if num == 0:
        return None, num
    return find_zero_tail, num - 1


# Iterative recurser
def tail_optimize(method, val):
    while method:
        method, val = method(val)
    return val

我不会尝试实现更具可读性的版本,因为我可能最终会:

def find_zero(val):
    return 0

但我认为在实际情况下,有一些很好的方法可以处理递归限制(无论是内存大小还是深度方面):

为了帮助内存(不深入),来自functools的lru_cache通常可以提供很多帮助:

>>> from functools import lru_cache
>>> @lru_cache()
... def fib(x):
...     return fib(x - 1) + fib(x - 2) if x > 2 else 1
... 
>>> fib(100)
354224848179261915075

对于堆栈大小,您可以使用listdeque,具体取决于您的上下文和用法,而不是使用语言堆栈。根据确切的实现(当您实际上在堆栈中存储简单的子计算以重复使用它们时),它被称为dynamic programming

>>> def fib(x):
...     stack = [1, 1]
...     while len(stack) < x:
...         stack.append(stack[-1] + stack[-2])
...     return stack[-1]
... 
>>> fib(100)
354224848179261915075

但是,使用您自己的结构而不是调用堆栈的好处是,您并不总是需要保持整个堆栈继续计算:

>>> def fib(x):
...     stack = (1, 1)
...     for _ in range(x - 2):
...         stack = stack[1], stack[0] + stack[1]
...     return stack[1]
... 
>>> fib(100)
354224848179261915075

但是,在尝试实施问题之前先了解一下&#34;&#34; (难以理解,难以调试,难以视觉化,代码不好,但很有趣):

>>> def fib(n):
...     return (4 << n*(3+n)) // ((4 << 2*n) - (2 << n) - 1) & ((2 << n) - 1)
... 
>>> 
>>> fib(99)
354224848179261915075

如果你问我,最好的实现是更具可读性的(对于Fibonacci示例,可能是具有LRU缓存的实现,但是通过更可读的if语句更改... if ... else ...,另一个例子是{ {1}}可能更具可读性,而对于其他示例,动态编程可能更好......

&#34;你正在为人类阅读你的代码写作,而不是为机器而写#34;