大多数人依赖堆栈“大”并且他们的程序没有使用所有堆栈,这仅仅是因为大小已设置得如此之大以至于程序很少会因为堆栈空间不足而失败,除非他们使用具有自动存储持续时间的非常大的数组。
这是一个工程故障,从某种意义上说,它不是工程:一个已知且很大程度上可预防的完全故障源是不受控制的。
通常,计算程序的实际堆栈需求可能很困难。特别是当存在递归时,编译器通常无法预测一个例程将被递归调用多少次,因此它无法知道该例程需要多少次堆栈空间。另一个复杂之处是调用在运行时准备的地址,例如调用虚函数或通过其他指向函数的指针。
但是,编译器和链接器可以提供一些帮助。对于使用固定数量的堆栈空间的任何例程,编译器理论上可以提供该信息。例程可能包括执行或未执行的块,并且每个块可能具有不同的堆栈空间要求。这会干扰为例程提供固定数量的编译器,但编译器可能会单独提供有关每个块的信息和/或例程的最大值。
理论上,链接器可以检查调用树,如果它是静态的且不是递归的,则可以为链接的程序提供最大的堆栈使用。他们还可以提供沿特定调用子链的堆栈使用(例如,从一个例程到导致递归调用同一例程的调用链),以便人类可以将算法知识应用于多个堆栈使用子链被递归调用的最大次数)。
我还没有看到具有这些功能的编译器或链接器。这表明开发这些功能几乎没有经济动机。
有时堆栈使用信息很重要。操作系统内核可能有一个比用户进程更有限的堆栈,因此应该计算内核代码的最大堆栈使用量(作为一种良好的工程实践),以便可以适当地设置堆栈大小(或重新设计代码使用更少的堆栈)。
如果您迫切需要计算堆栈空间要求,则可以检查编译器生成的汇编代码。在许多计算平台上的许多例程中,在例程开始时从堆栈指针中减去一个固定数字。在没有额外的减法或“推”指令的情况下,这是例程的堆栈使用,不包括它调用的子例程使用的进一步堆栈。但是,例程可能包含包含额外堆栈分配的代码块,因此您必须小心检查生成的汇编代码以确保您已找到所有堆栈调整。
例程也可能包含在运行时计算的堆栈分配。在计算堆栈空间至关重要的情况下,您可能会避免编写导致此类分配的代码(例如,避免使用 C 的可变长度数组功能)。
一旦确定了每个例程的堆栈使用,您可以通过沿各种例程调用路径添加每个例程的堆栈使用来确定程序的总堆栈使用(包括调用之前运行的启动例程的堆栈使用main
) .
这种对完整程序的堆栈使用的计算通常很困难,很少执行。
您通常可以通过了解程序“需要”多少数据来完成其工作来估计程序的堆栈使用情况。每个例程通常都需要堆栈空间来存储它使用的对象,并具有自动存储持续时间以及一些用于保存处理器寄存器、将参数传递给子例程、一些临时工作等的开销。许多事情可以改变堆栈的使用,因此只能通过这种方式获得估计。例如,您的示例程序不需要任何空间来存放number
. 由于number
从未打印过声明或使用的结果,因此编译器中的优化器可以消除它。您的程序只需要用于启动例程的堆栈空间;该main
例程不需要做任何事情,除了返回零。