递归函数、堆栈溢出和 Y 组合器
我有一个递归函数(在 C# 中),我需要调用大约 8 亿次;这显然通常会在大约第 900 次调用后导致堆栈溢出。我已将其踢出多个循环,但递归模式更容易维护,也更干净。
我正在考虑使用 y 组合器实现递归函数,从我正在阅读和看到的内容来看,这应该可以解决堆栈溢出问题,并修复多个嵌套循环。
有人有使用 y-combinator 的经验吗?我还会陷入堆栈溢出吗?
以阶乘为例。大多数大于 5,000 的数字的阶乘将导致堆栈溢出。如果我在这种情况下正确使用 y 组合器,它会修复堆栈溢出吗?
实现起来似乎并不简单,所以我想在花费开发精力/资源来实现和学习 y-combinator 之前确认一下。
I have a recursive function (in C#) that I need to call about 800 million times; this would obviously normally result in a stack overflow after about the 900th call. I've kicked this out to multiple loops, but the recursive pattern is just so much easier, and cleaner to maintain.
I'm looking at implementing the recursive function using a y-combinator, as from what I'm reading and seen, that should solve the stack overflow problem, and fix the multiple nested loops.
Does anyone have experience using the y-combinator? Will I still be stuck in a stack overflow?
Take the simple example of a factorial. A factorial on most numbers bigger than like 5,000 will cause a stack overflow. If I used a y-combinator properly in that scenario, would it fix the stack overflow?
It doesn't seem trivial to implement, so I want to confirm it before I spend the development effort/resources implementing and learning the y-combinator.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
不,使用 Y 组合器对您的情况没有帮助。如果您需要执行某件事 8 亿次,您可以 (a) 递归,或 (b) 循环(或者我认为 (c) 对您的函数写入 8 亿次调用)。由于 Y 组合器不循环,因此它必须递归。
循环就是您要寻找的,除非您使用提供适当尾递归的运行时环境(例如Scheme)。
话虽如此,用您选择的语言从头开始实现 Y 组合器是一个很好的练习。
No, using the Y-combinator will not help your situation. If you need to do something 800 million times, you can either (a) recurse, or (b) loop (or I suppose (c) write 800 million calls to your function). Since the Y-combinator doesn't loop, it must therefore recurse.
A loop is what you're looking for, unless you're using a runtime environment that offers proper tail recursion (such as Scheme).
Having said that, implementing the Y-combinator from scratch in the language of your choice is an excellent exercise.
Y 组合器很有用,但我认为您确实需要尾递归优化,从而消除尾递归函数中堆栈的使用。仅当调用者立即返回每次递归调用的结果并且调用后不再进行任何额外计算时,尾递归才可能。不幸的是,C# 不支持尾递归优化,但是您可以使用蹦床来模拟它,这允许从尾递归方法到蹦床包装方法的简单转换。
Y combinators are useful but I think you really want tail recursion optimization that eliminates the use of the stack for tail recursive functions. Tail recursion is only possible when the result of every recursive call is immediately returned by the caller and never any additional computation after the call. Unfortunately C# does not support tail recursion optimization however you can emulate it with a trampoline which allows for a simple conversion from tail recursive method to a trampoline wrapped method.
您可以使用 Reactive Extension 中使用的 Trampoline,我已尝试在我的博客上解释它 http://rohiton.net/2011/08/15/trampoline-and-reactive-extensions/
You can use trampoline as is used in Reactive Extension, I have tried to explain it on my blog http://rohiton.net/2011/08/15/trampoline-and-reactive-extensions/
一种解决方案是将函数转换为使用循环和显式上下文数据结构。上下文代表人们通常认为的调用堆栈。您可能会发现我对另一个问题的回答关于转换为尾递归很有用。相关步骤为1至5; 6 和 7 是特定于该特定功能的,而您的听起来更复杂。
不过,“简单”的替代方案是为每个函数添加一个深度计数器;当它达到某个限制(通过实验确定)时,生成一个新线程以使用新堆栈继续递归。旧线程会阻塞等待新线程向其发送结果(或者如果您想要更奇特的话,可以发送结果或重新引发异常)。对于新线程,深度计数器返回到 0;当达到限制时,创建第三个线程,依此类推。如果您缓存线程以避免不必要地频繁地支付线程创建成本,那么您将有望获得可接受的性能,而无需大幅重组程序。
One solution is to convert your function(s) to use a loop and an explicit context data structure. The context represents what people generally think of as the call stack. You might find my answer to another question about converting to tail recursion useful. The relevant steps are 1 through 5; 6 and 7 are specific to that particular function, whereas yours sounds more complicated.
The "easy" alternative, though, is to add a depth counter to each of your functions; when it hits some limit (determined by experimentation), spawn a new thread to continue the recursion with a fresh stack. The old thread blocks waiting for the new thread to send it a result (or if you want to be fancy, a result or an exception to re-raise). The depth counter goes back to 0 for the new thread; when it hits the limit, create a third thread, and so on. If you cache threads to avoid paying the thread creation cost more often than necessary, hopefully you'll get acceptable performance without having to drastically restructure your program.