通过在堆上分配堆栈部分来避免堆栈溢出?

时间:2014-01-28 18:27:38

标签: c recursion stack stack-overflow heap-memory

是否有一种语言可以启用一种机制,在超出原始堆栈空间时在堆上分配新的堆栈空间?

我记得在我的大学做了一个实验室,我们在C中使用内联汇编来实现基于堆的可扩展堆栈,所以我知道它原则上应该是可行的。

我理解在开发应用程序时获取堆栈溢出错误可能很有用,因为它会在不使系统占用大量内存并开始交换的情况下快速终止疯狂的无限递归。

但是,当你有一个经过良好测试的完成的应用程序要部署并且希望它尽可能健壮时(比如它是在台式计算机上运行的非常关键的程序),那么知道它会很好在堆栈更有限的一些其他系统上,一些对象占用更多空间,或者如果程序面临一个需要更多堆栈内存的特殊情况而不是在测试中,它将不会失败。

我认为正是由于这些陷阱,通常在生产代码中避免了递归。但是如果我们在生产代码中有一个自动堆栈扩展机制,那么我们就可以使用递归来编写更优雅的程序,因为知道它不会意外地出现段错误,而系统有16 GB的堆内存可供使用... < / p>

5 个答案:

答案 0 :(得分:4)

有先例。

  • GHC(Haskell编译器)的运行时使用堆而不是堆栈。堆栈仅在您调用外国代码时使用。

  • Google的Go实现使用goroutines的分段堆栈,根据需要扩大堆栈。

  • Mozilla的Rust 习惯使用分段堆栈,虽然决定它引起的问题多于解决的问题(参见[rust-dev] Abandoning segmented stacks in Rust)。

  • 如果内存服务,一些Scheme实现将堆栈帧放在堆上,然后像其他对象一样垃圾收集帧。

在命令式语言的传统编程风格中,大多数代码都会避免递归调用自身。堆栈溢出在野外很少见,并且它们通常由草率编程或恶意输入触发 - 尤其是递归下降解析器等,这就是为什么一些解析器在嵌套超过阈值时拒绝代码的原因。 p>

避免生产代码中堆栈溢出的传统建议:

  1. 不要编写递归代码。 (例如:重写搜索算法以使用显式堆栈。)

  2. 如果您确实编写了递归代码,请证明递归是有界的。 (例如:搜索平衡树的大小取决于树大小的对数。)

  3. 如果您无法证明它是无界的,请添加一个绑定。 (示例:添加对解析器支持的嵌套量的限制。)

答案 1 :(得分:2)

我不相信你会找到强制执行此操作的语言。但是特定的实现可以提供这样的机制,并且根据操作系统的不同,运行时环境很可能会根据需要自动扩大堆栈。

答案 2 :(得分:2)

根据gcc的文档,如果使用gcc选项进行编译,-fsplit_stack可以生成执行此操作的代码:

-fsplit-stack
     Generate code to automatically split the stack before it overflows.
     The resulting program has a discontiguous stack which can only
     overflow if the program is unable to allocate any more memory.
     This is most useful when running threaded programs, as it is no
     longer necessary to calculate a good stack size to use for each
     thread.  This is currently only implemented for the i386 and
     x86_64 backends running GNU/Linux.

     When code compiled with -fsplit-stack calls code compiled
     without -fsplit-stack, there may not be much stack space
     available for the latter code to run.  If compiling all code,
     including library code, with -fsplit-stack is not an option,
     then the linker can fix up these calls so that the code compiled
     without -fsplit-stack always has a large stack.  Support for
     this is implemented in the gold linker in GNU binutils release 2.21
     and later.

llvm代码生成框架为分段堆栈提供支持,这些堆栈以go语言使用,最初在Mozilla的rust中使用(尽管它们在生地上被删除了对于“高性能语言”,执行开销太高了。(参见this mailing list thread

尽管rust - 团队的反对意见,分段堆栈的速度却出乎意料地快,尽管堆栈抖动问题会影响特定程序。 (有些Go程序也会遇到这个问题。)

Henry Baker在1994年的论文Cheney on the MTA中提出了以相对有效的方式堆栈分配堆栈段的另一种机制,并成为Chicken Scheme运行时的基础,主要编译为R 5 RS兼容方案实施。

答案 3 :(得分:0)

在生产代码中,递归肯定是 - 它只是在适当的地方和时间使用。

如果你担心它,正确的答案可能不仅仅是在向量中切换到手动维护的堆栈或其他任何东西 - 尽管你可以这样做 - 但重新组织逻辑。例如,我正在研究的编译器用工作列表驱动的进程替换了一个深度递归进程,因为不需要按处理顺序维护严格的嵌套,只是为了确保我们依赖的变量被计算出来在使用之前。

答案 4 :(得分:0)

如果链接到线程库(例如C中的pthreads),则每个线程都有一个单独的堆栈。这些堆栈以这种或那种方式分配,最终(在UNIX环境中)使用brk或匿名mmap。这些可能会或可能不会在途中使用堆。

我注意到以上所有答案都是指单独的堆栈;没有明确说“在堆上”(在C意义上)。我认为海报只是意味着“来自动态分配的内存”,而不是调用处理器堆栈。