【问题标题】:How to know how much stack function is consuming?如何知道堆栈函数消耗了多少?
【发布时间】:2015-01-28 00:10:54
【问题描述】:

最近,我在一次采访中遇到了这个问题:
我们如何确定特定函数在堆栈上消耗了多少存储空间?

【问题讨论】:

  • C 和 C++ 标准没有堆栈空间的概念。任何解决方案都必须依赖于实现。
  • 有点恼人的是,面试候选人的公司确实如此关心实施。无论如何,如果您从未查看过编译器生成的机器代码或不知道它的含义,那么您不太可能在那里找到工作。你是怎么知道的。
  • 在运行时还是通过观察代码?

标签: c++ c stack


【解决方案1】:

众所周知,“堆栈”是平台的一个实现细节,无法从语言本身检查或以任何方式查询。在 C 或 C++ 程序的任何部分中,基本上不可能保证是否可以进行另一个函数调用。 “堆栈大小”,或者更好地称为“函数调用和局部变量存储深度”,是语言标准承认其存在但被认为超出范围的实现限制之一。 (例如,对于 C++,请参阅 [implimits],附件 B。)

个别平台可能会提供 API 以允许程序自省平台限制,但 C 和 C++ 均未指定这一点或如何实现。

超出实现定义的资源限制会导致未定义的行为,并且您无法知道是否会超出限制。

【讨论】:

  • 虽然都是真的,但我认为您可能误解了这个问题。我认为你可以忽略调用深度——他只想知道当前堆栈帧有多大。
【解决方案2】:

它是完全由实现定义的——该标准不会以任何方式对程序可能使用的底层机制提出要求。

在 x86 机器上,一个堆栈帧由返回地址(4/8 字节)、参数和局部变量组成。

参数,例如标量可以通过寄存器传递,所以我们不能确定它们是否有助于占用的存储空间。当地人可能会被填充(而且经常是);我们只能推断出这些的最小存储量。

唯一确定的方法是实际分析编译器生成的汇编代码,或查看运行时堆栈指针值的绝对差异 - 在调用特定函数之前和之后。

例如

#include <iostream>

void f()
{
    register void* foo asm ("esp");
    std::cout << foo << '\n';
}

int main()
{
    register void* foo asm ("esp");
    std::cout << foo << '\n';
    f();
}

现在比较输出。 GCC on Coliru 给了

0x7fffbcefb410
0x7fffbcefb400

相差 16 个字节。 (堆栈在 x86 上向下增长。)

【讨论】:

    【解决方案3】:

    正如其他答案所述,程序堆栈是一个未在语言本身中指定的概念。但是,了解典型实现的工作原理后,您可以假设函数的第一个参数的地址是其堆栈帧的开头。下一个调用函数的第一个参数的地址是下一个堆栈帧的开始。所以,他们可能希望看到这样的代码:

    void bar(void *b) {
       printf("Foo stack frame is around %lld bytes\n", llabs((long long)b - (long long)&b));
    }
    
    void foo(int x) {
      bar(&x);
    }
    

    【讨论】:

      【解决方案4】:

      对于那些使用堆栈的实现,堆栈的大小增加是:

      • 不适合可用寄存器的变量大小
      • 在预先声明的函数中声明的变量的大小在函数的整个生命周期内都存在
      • 沿途或语句块中声明的其他局部变量的大小
      • 此函数调用的函数使用的最大堆栈大小
      • 以上所有内容*递归调用的次数
      • 返回地址的大小

      退货地址

      大多数实现在任何其他数据之前将返回地址压入堆栈。所以这个地址占用空间。

      可用寄存器

      有些处理器有很多寄存器;但是,可能只有少数可用于传递变量。例如,如果约定允许 2 个变量但有 5 个参数,则 3 个参数将放在堆栈上。

      大对象传值时,会占用栈空间。

      函数局部变量

      这很难计算,因为变量可能会被压入堆栈,然后在不使用时弹出。

      某些变量在声明之前可能不会被压入堆栈。因此,如果一个函数在中途返回,它可能不会使用剩余的变量,因此这些变量的堆栈大小不会增加。

      编译器可以选择使用寄存器来保存值或将常量直接放入可执行代码中。在这种情况下,它们不会向堆栈添加任何长度。

      调用其他函数

      该函数可能会调用其他函数。每个调用的函数都可能增加堆栈上的数据量。那些被调用的函数可能会调用其他函数,依此类推。

      这又取决于执行时的快照。然而,一个可以通过其他调用的函数产生一个近似的最大堆栈增量。

      递归

      与调用其他函数一样,递归调用可能会增加堆栈的大小。在函数末尾的递归调用可能会比靠近开头的递归调用增加更多的堆栈。

      注册值保存

      有时,编译器可能需要比分配的寄存器允许的更多空间来存储数据。因此编译器可以将变量压入堆栈。

      为方便起见,编译器可能会将寄存器压入堆栈,例如交换寄存器或更改值的顺序。

      总结

      函数所需的堆栈空间的确切大小很难计算,并且可能取决于执行的位置。在堆栈大小计算中需要考虑许多项目,例如参数数量和大小以及调用的任何其他函数。由于可变性,大多数堆栈大小测量基于最大大小,或最坏情况大小。堆栈分配通常基于最坏的情况。

      对于面试问题,我会提到以上所有内容,这通常会让面试官想要快速进入下一个问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-11-04
        • 2019-02-11
        • 2011-07-11
        • 2010-09-06
        • 1970-01-01
        • 1970-01-01
        • 2016-07-07
        相关资源
        最近更新 更多