【问题标题】:Recursion overhead in python vs C [closed]python vs C中的递归开销[关闭]
【发布时间】:2018-08-27 16:47:50
【问题描述】:

递归涉及大量开销是python中的一个众所周知的问题。如此之多,即使是相对较浅的递归调用也会导致程序崩溃。一种可能的解决方法是在 C 中实现花哨的递归算法,并提供 python 钩子。但是,众所周知,算法的递归实现几乎无论如何都比循环更昂贵,除非您的编译器识别出一些非常具体的机会(例如尾递归)来循环整个业务.

Anyhoo,由于递归涉及函数调用的开销,我想知道在 python 和 C 中函数调用(以及递归)的相对开销是多少?我意识到我可以timeit,但我希望有一个原则性的解释。 python函数调用与C函数调用的全部内容是什么?另外,python 和 C 之间的函数调用堆栈是否存在差异会影响深度调用堆栈的性能?

【问题讨论】:

  • 您想在 C 中实现递归算法,以便在堆栈溢出时得到一个段错误而不是一个很好的异常? =P
  • 任何递归过程都可以使用迭代重新实现;如果堆栈深度限制是一个问题,只需重新设计您的算法。
  • 我不会说这是导致递归 Python 程序崩溃的开销。我认为这是一个有意的设计选择。在导致实际堆栈溢出之前,您可以将maximum recursion depth 提高几个数量级。或者您是在说“即使将递归限制设置得非常高,Python 中的堆栈溢出也比 C 中发生得更快?”?我想这是真的,但是对于大多数建立在 C 之上的语言来说,这可能是真的。
  • 我不知道递归函数调用比任何其他函数调用更昂贵,无论是在 Python 中还是在 C 中。
  • 如果你出于性能原因用 C 重写它,那你为什么坚持用递归来写呢?编写递归函数的主要原因是它使代码干净、简短且易于阅读。

标签: python c


【解决方案1】:

你从一组错误的前提开始:

递归涉及大量开销是python中的一个众所周知的问题。

不,不是。递归函数调用的开销与任何其他函数调用的开销基本相同。可能有一个小的区别,你不断分配新的堆栈帧并将它们加载到缓存中,而使用内部非递归调用的循环实现的相同事物将能够重用相同的堆栈帧。但它会非常小。

如此之多,即使是相对较浅的递归调用也会导致程序崩溃。

不,他们没有。超过 1000 深度的递归完全失败,但有异常。这部分是为了防止此类崩溃,但也是为了更容易检测意外的无限递归。

一种可能的解决方法是在 C 中实现花哨的递归算法

这无济于事。大多数 C 实现处理递归的方式基本上与大多数 Python 实现相同(事实上。在 CPython 中,递归可以只是 C 递归主 eval 循环的问题)。仍然没有消除尾调用,也没有特殊处理来仅保持帧中的活动变量处于活动状态等。虽然 C 堆栈帧比 Python 堆栈帧小一点,但这只是一个小的乘法因子。

同时,与 Python 程序不同,如果递归太深,C 程序实际上会崩溃。


当然,当您正在做的是低级计算时,C 代码确实比 Python 代码快一两个数量级,这将像在其他任何地方一样为您提供帮助(尽管可能不是和往常一样,因为 C 中的函数调用虽然没有 Python 慢,但仍然远非免费)。另外,C 确实迫使您对某些事情更加明确——例如,没有闭包,所以如果你想要 nonlocal 的效果,你需要将指针传递给本地人——这可能会导致你优化你的东西在 Python 中不会想到。

但总的来说,这里的好处比非递归代码少。


在某些情况下,一个好的 C 优化器可以看到整个框架无用并丢弃它。在某些情况下,它甚至可以完全消除尾声。这不如你可以依靠的通用尾调用消除来确保正确性,但它肯定很有帮助。

但一个好的 JIT 优化器有时也可以做同样的事情。在 PyPy 或 Jython 中运行现有代码肯定比在 C 中重写要简单得多。

(我确信在某些情况下,例如,LLVM 的 AOT 优化器可以提供帮助,但 PyPy 的 JIT 不能,但我也怀疑任何能够可靠猜测这些情况的人都不会在第一名。)


种语言比 Python 和 C 更好地处理递归。

如果您在 Haskell 中编写算法代码,然后将其封装在 C API 中,您可以通过 ctypes 或通过 C 语言编写的简单扩展调用该 API(或者甚至将其编译为程序和子进程)它,如果你说的是需要几秒钟才能运行的东西);那会工作的。


但是,如果您改为将递归重写为带有显式堆栈的循环,您将消除与在并非旨在鼓励递归的语言(Python 和 C)中使用递归相关的所有问题,并打开其他优化的大门。

例如,您(或 LLVM 的优化器,或 PyPy 的 JIT)可以在内循环的中间内联函数调用,而递归调用显然无法做到这一点。

此外,无需跨 Python 边界来回调用,提取代码片段通常会变得更容易,因此您可以将瓶颈移植到 C 扩展模块,而无需移植整个代码。

【讨论】:

  • "在 CPython 中,递归可以只是 C 递归主 eval 循环的问题"这只是取决于我编译 python 时的选项,还是这里发生了一些黑魔法?跨度>
  • @Scott 在 CPython 中,没有办法让它永远不使用 C 堆栈。但是,只需对代码进行一些小的更改即可。这就是 Stackless Python(以及 PyPy 的 Stackless 分支)所做的。
  • 例如,gcc.godbolt.org/z/VBXz0C(其中 clang 不仅找到了尾调用优化,而且还将递归展开了 8 倍)。 GCC、ICC 和 MSVS 都会产生一个简单的循环(只要优化级别足够高;分别为 -O2、-O1 和 /O2)。
  • @rici 好的,但是如果您的问题是“我需要 TCO 或者我的算法不正确”,那么答案不是“我会用 C 重写我的 Python 并希望许多编译器可以经常消除尾声”。您要么重写算法以不需要 TCO,要么用一种为尾部调用定义了保证的语言编写它。
  • @abarnert:我同意。但是说“你不能依赖 C 来优化尾调用”和说根本没有尾调用优化是有区别的。 Python 根本不优化尾调用。但事实上,如果启用优化,大多数流行的 C 编译器会优化许多尾调用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-08-27
  • 2017-01-25
  • 2022-01-15
  • 2016-04-03
  • 2018-08-15
  • 2018-05-20
  • 2010-10-31
相关资源
最近更新 更多