在 C90 中实现无溢出的系统堆栈
Posted
技术标签:
【中文标题】在 C90 中实现无溢出的系统堆栈【英文标题】:Implementing an overflow-free system stack in C90 【发布时间】:2011-08-05 05:04:12 【问题描述】:我刚刚阅读了 Google Go 如何在默认情况下使每个线程的堆栈大小减小,然后在发生溢出时链接到新堆栈(请参阅 here 中的第 16 页)。我想知道在 C 中做到这一点的最佳方法。
我不得不说我不是 C 专家,所以可能有更好的方法来检测 C 上的堆栈溢出,但鉴于我的无知,以下是我认为我会如何实现它:
我想到的第一件事是,每次我们有一个新的堆栈时,我们都会得到一个堆栈变量的地址,然后我们就大致有了起始堆栈地址。然后我们需要能够检索线程有多少堆栈空间。如果线程不是主线程,这是可能的,但我不知道我们如何在 C 上获取这些信息。
然后我们需要通过检索当前堆栈变量地址来检查(每个函数调用,可能是)已经使用了多少堆栈。如果我们检测到可能的堆栈溢出,我们需要有一些方法来创建一个新堆栈并链接到最后一个堆栈。我认为可以在 C 中完成的唯一方法是创建一个新线程来执行我们想要的函数,并锁定当前线程直到函数返回其结果。
那么,是否有更清洁/更好的方法来实现这一点?
【问题讨论】:
我不确定我是否喜欢你的反溢出态度。你确定你没有在错误的网站上;) 请注意,拆分堆栈没有什么是无溢出的。malloc
(或用于分配新堆栈片段的任何方法)仍然可能失败,并且应用程序无法检测和处理此问题。
【参考方案1】:
请参阅 GCC 的 split-stack capability。我相信这最初是为了支持 Go 而实现的。按照您的建议,它几乎可以正常工作。
编辑:下面的评论讨论了另一个对激活记录进行堆分配的系统。
【讨论】:
我不认为你“确定”了堆栈大小。您在每个[堆栈分配点所做的是确定您要分配多少堆栈(我认为拆分堆栈方案使用 1 个 VM 页面)并将该大小存储在已知的某处(例如,如果它是,则在堆栈底部)两个大小的幂)。拆分堆栈方案,如果它总是分配一个固定大小的块,简单地知道每个段有多大。 我知道这个链接的原因是我已经构建了一个非C语言(PARLANSE),它实现了堆分配的堆栈片段,所以我一直对这种方案很感兴趣。 PARLANSE 为 每个 函数调用分配一个新片段,我认为价格与 GCC 方法差不多。根据我们的测量,开销约为 3-5%,但我们的语言和编码风格倾向于鼓励大函数(一个好的 C 编译器也会这样做,通过内联所有小函数)。请参阅 www.semanticdesigns.com/Products/PARLANSE 不,PARLANSE 比这更疯狂。它旨在允许 数百万 个计算粒度全部并行运行,这是“大堆栈模型”无法做到的(请参阅 ***.com/questions/1016218/…)。如果每个grain 都可以分叉并行子工作,并且分叉的grain 可以互锁资源,则您希望这样做。因此,PARLANSE 将每个 CPU 的 1 个操作系统线程多路复用到数百万个正在运行的“颗粒”上。在实践中,它会尝试将粒数保持在比线程大得多的数字,这足以有效。 你从哪里得到足够的工作来证明这一点?我们使用 PARLANSE 来处理完整的源代码系统(参见 www.semanticdesigns.com/Products/DMS/DMSToolkit.html),这需要庞大的数据结构和大量的分析。更好的并行比没有。 PARLANSE 并不适合这项工作,但它比我们见过的任何其他东西都要好:- 开销是速度差异。内存开销大约为 25%,因为我们使用了两个大小的块的伙伴系统能力。每个函数都知道它(以及它的并行子粒度)需要多少堆栈空间,并要求下一个大小为 2 的幂。真正的诀窍在于拥有一堆不需要锁来分配的可用块:- 我们使用大量线程本地存储来提高效率。【参考方案2】:你可以这样做——我相信现代 gcc 甚至可以选择它——但它大大增加了函数调用的成本并且几乎没有实际好处。尤其是在具有 64 位寻址的现代系统上,每个线程都有足够的地址空间来拥有自己的堆栈,远离其他线程的堆栈。如果您发现自己使用的不仅仅是对数尺度的调用递归,那么无论如何您的算法都有问题...
【讨论】:
“非常”是什么意思?据我所知,它最多需要一个新的临时变量推送和比较。好吧,当你想到例如一个网络服务器,每个线程都有一个小的堆栈空间是非常重要的! 不会大大增加函数调用的开销。与内置在 CPU 中的标准调用相比,它只需要几条指令,而且最有趣的函数有数百条指令长。 (如果编译器擅长内联,较短的函数往往会消失)。在东,这是我的个人经历;请参阅我的答案。 对于网络服务器,您已经可以为每个线程拥有一个小堆栈。只需使用pthread_attr_setstacksize(&attr, getconf(_SC_THREAD_STACK_MIN));
,不要编写愚蠢的递归代码。
至于函数大小,结构良好的代码使用小而简单的函数,并且使用共享库和带有访问函数的不透明类型,编译器无法优化调用。即使没有像拆分堆栈这样的额外丑陋浪费,我已经遇到了 gcc 的调用开销(堆栈对齐和在不需要它的情况下无用的寄存器保存之类的东西)导致总运行时间的 10-30% 的情况功能。使用拆分堆栈时,这很容易跃升至 60-90%。
另请注意,如果代码良好,编译器可以轻松执行静态分析,以确定每个线程启动函数所需的确切堆栈大小。只有使用递归或任意回调函数的代码才能在所需的堆栈空间上建立编译时界限。以上是关于在 C90 中实现无溢出的系统堆栈的主要内容,如果未能解决你的问题,请参考以下文章