3

如何计算我的程序在 UNIX 中所需的最小堆栈大小,以便我的程序永远不会崩溃。

假设我的程序是

int main()
{
         int number;
         number++;
         return 0;
}

1) 这个程序运行所需的堆栈大小是多少?它是如何计算的?

2) 我的 Unix 系统给出ulimit -s 512000. 512MB我的小程序真的需要这个值吗?

3) 如果我有一个具有多线程的大程序,大约 500 个函数,包括一些库、宏、动态分配的内存等。为此需要多少堆栈大小?

4

3 回答 3

3
  1. 您的程序本身使用了几个字节 - 1 个 int,但当然还有之前的运行时部分main需要考虑在内。但它不可能超过几十个字节,可能一次有几百个字节。由于任何现代操作系统中的最小堆栈大小是“一页”= 4KB,这应该很容易适应。
  2. 51200 = 51.2MB,但这似乎相当高。在我的 Linux Fedora 16 x86-64 机器上,它是 8192。
  3. 线程并不重要,因为每个线程都有自己的堆栈。函数的数量本身并不是堆栈使用的巨大贡献者。用完堆栈几乎总是由大型局部变量和/或深度递归引起的。对于任何有点复杂的程序,计算精确的堆栈使用量可能非常棘手。通常,它涉及大量运行程序并查看堆栈是否“爆炸”。如果没有,你有足够的筹码。一般来说,库函数往往不会使用大量的堆栈,但总有例外。

举例:

void func()
{
   int x, y, z;
   float w;
   ...
}

此函数占用大约 16 字节的堆栈,加上调用函数的一般开销,通常为 1-3 个“机器字”(32 位机器上为 4-12 字节,64 位机器上为 8-24 字节) .

void func2()
{
    int x[10000];
    ...
}

此函数将占用 40000 字节的堆栈空间。显然,您不需要对该函数进行多次递归调用即可耗尽堆栈。

于 2013-08-28T09:36:23.637 回答
2

没有什么神奇的方法可以告诉你你的程序在堆栈上需要多少空间。这取决于代码实际在做什么。即使程序似乎没有做任何事情,无限(或非常深)递归也会导致堆栈溢出。

例如,请参见以下内容:

$ ulimit
unlimited
$ echo "foo(){foo();} main(){foo();}" | gcc -x c -
$ ./a.out 
Segmentation fault (core dumped)
于 2013-08-28T09:28:17.480 回答
1

大多数人依赖堆栈“大”并且他们的程序没有使用所有堆栈,这仅仅是因为大小已设置得如此之大以至于程序很少会因为堆栈空间不足而失败,除非他们使用具有自动存储持续时间的非常大的数组。

这是一个工程故障,从某种意义上说,它不是工程:一个已知且很大程度上可预防的完全故障源是不受控制的。

通常,计算程序的实际堆栈需求可能很困难。特别是当存在递归时,编译器通常无法预测一个例程将被递归调用多少次,因此它无法知道该例程需要多少次堆栈空间。另一个复杂之处是调用在运行时准备的地址,例如调用虚函数或通过其他指向函数的指针。

但是,编译器和链接器可以提供一些帮助。对于使用固定数量的堆栈空间的任何例程,编译器理论上可以提供该信息。例程可能包括执行或未执行的块,并且每个块可能具有不同的堆栈空间要求。这会干扰为例程提供固定数量的编译器,但编译器可能会单独提供有关每个块的信息和/或例程的最大值。

理论上,链接器可以检查调用树,如果它是静态的且不是递归的,则可以为链接的程序提供最大的堆栈使用。他们还可以提供沿特定调用子链的堆栈使用(例如,从一个例程到导致递归调用同一例程的调用链),以便人类可以将算法知识应用于多个堆栈使用子链被递归调用的最大次数)。

我还没有看到具有这些功能的编译器或链接器。这表明开发这些功能几乎没有经济动机。

有时堆栈使用信息很重要。操作系统内核可能有一个比用户进程更有限的堆栈,因此应该计算内核代码的最大堆栈使用量(作为一种良好的工程实践),以便可以适当地设置堆栈大小(或重新设计代码使用更少的堆栈)。

如果您迫切需要计算堆栈空间要求,则可以检查编译器生成的汇编代码。在许多计算平台上的许多例程中,在例程开始时从堆栈指针中减去一个固定数字。在没有额外的减法或“推”指令的情况下,这是例程的堆栈使用,不包括它调用的子例程使用的进一步堆栈。但是,例程可能包含包含额外堆栈分配的代码块,因此您必须小心检查生成的汇编代码以确保您已找到所有堆栈调整。

例程也可能包含在运行时计算的堆栈分配。在计算堆栈空间至关重要的情况下,您可能会避免编写导致此类分配的代码(例如,避免使用 C 的可变长度数组功能)。

一旦确定了每个例程的堆栈使用,您可以通过沿各种例程调用路径添加每个例程的堆栈使用来确定程序的总堆栈使用(包括调用之前运行的启动例程的堆栈使用main) .

这种对完整程序的堆栈使用的计算通常很困难,很少执行。

您通常可以通过了解程序“需要”多少数据来完成其工作来估计程序的堆栈使用情况。每个例程通常都需要堆栈空间来存储它使用的对象,并具有自动存储持续时间以及一些用于保存处理器寄存器、将参数传递给子例程、一些临时工作等的开销。许多事情可以改变堆栈的使用,因此只能通过这种方式获得估计。例如,您的示例程序不需要任何空间来存放number. 由于number从未打印过声明或使用的结果,因此编译器中的优化器可以消除它。您的程序只需要用于启动例程的堆栈空间;该main例程不需要做任何事情,除了返回零。

于 2013-08-28T11:41:20.783 回答