C ++堆栈和范围

时间:2009-08-27 19:07:34

标签: c++ optimization stack

我在Visual C ++ 2008上尝试了这段代码,它显示A和B没有相同的地址。

int main()
{
    {
        int A;
        printf("%p\n", &A);
    }

    int B;
    printf("%p\n", &B);
}

但是,当B被定义时,A不再存在,在我看来,相同的堆栈位置可以重复使用...

我不明白为什么编译器看起来不像一个非常简单的优化(例如在较大的变量和递归函数的上下文中可能很重要)。并且似乎重用它不会在CPU和内存上更重。有人对此有解释吗?

我猜答案的答案是“因为它比看起来要复杂得多”,但老实说我不知道​​。

编辑:关于以下答案和评论的一些准确性。

这段代码的问题在于,每次调用此函数时,堆栈都会“增加一个整数”。当然这在示例中没有问题,但考虑大变量和递归调用,并且您可以轻松避免堆栈溢出。

我建议的是内存优化,但我不知道它会如何损害性能。

顺便说一句,这种情况发生在发布版本中,所有优化都会在。

7 个答案:

答案 0 :(得分:8)

重用像这样的本地人的堆栈空间是一种非常常见的优化。事实上,在优化的构建中,如果你没有获取本地的地址,编译器甚至可能不会分配堆栈空间,变量只会存在于寄存器中。

由于多种原因,您可能看不到此优化。

首先,如果优化已关闭(如调试版本),编译器将不会执行其中任何一项以使调试更容易 - 即使在函数中不再使用A,也可以查看A的值。

如果您正在使用优化进行编译,我的猜测是因为您正在获取本地的地址并将其传递给另一个函数,编译器不希望重用该存储,因为它不清楚该函数在做什么地址。

除非函数使用的堆栈空间超过某个阈值,否则也可以设想不使用此优化的编译器。我不知道有任何编译器这样做,因为重用不再使用的局部变量空间的成本为零,可以全面应用。

如果堆栈增长是您的应用程序的一个严重问题,即在某些情况下您遇到堆栈溢出,则不应该依赖编译器的堆栈空间优化。您应该考虑将堆栈上的大缓冲区移动到堆上,并努力消除非常深的递归。例如,在Windows上,线程默认具有1 MB堆栈。如果你担心溢出,因为你在每个堆栈帧上分配1k内存并进行1000次递归调用,修复不是试图诱使编译器从每个堆栈帧中节省一些空间。

答案 1 :(得分:3)

为什么不查看装配?

我稍微更改了你的代码,以便int A = 1;和int B = 2;使其更容易破译。

使用默认设置从g ++:

    .globl main
    .type   main, @function
main:
.LFB2:
    leal    4(%esp), %ecx
.LCFI0:
    andl    $-16, %esp
    pushl   -4(%ecx)
.LCFI1:
    pushl   %ebp
.LCFI2:
    movl    %esp, %ebp
.LCFI3:
    pushl   %ecx
.LCFI4:
    subl    $36, %esp
.LCFI5:
    movl    $1, -8(%ebp)
    leal    -8(%ebp), %eax
    movl    %eax, 4(%esp)
    movl    $.LC0, (%esp)
    call    printf
    movl    $2, -12(%ebp)
    leal    -12(%ebp), %eax
    movl    %eax, 4(%esp)
    movl    $.LC0, (%esp)
    call    printf
    movl    $0, %eax
    addl    $36, %esp
    popl    %ecx
    popl    %ebp
    leal    -4(%ecx), %esp
    ret
.LFE2:

最终,看起来编译器似乎没有费心将它们放在同一个地址中。没有花哨的前瞻优化。要么它没有尝试优化,要么它决定没有任何好处。

注意A已分配,然后打印。然后分配和打印B,就像在原始来源中一样。当然,如果您使用不同的编译器设置,这看起来可能完全不同。

答案 2 :(得分:2)

据我所知,B的空间是在进入main时保留的,而不是在

int B;

如果你在该行之前打破调试器,你仍然可以获得B的地址。在此行之后,stackpointer也不会改变。在这一行发生的唯一事情就是调用B的构造函数。

答案 3 :(得分:1)

可能是编译器将两者放在同一个堆栈帧上。因此,即使A在其范围之外无法访问,编译器也可以自由地将其绑定到内存中的某个位置,只要它不会破坏代码的语义。简而言之,它们会在您执行主体的同时将两者放在堆栈中。

答案 4 :(得分:1)

在之后在堆栈上分配A B.在代码中的A之后声明了B(顺便说一下C90不允许),但它仍然在主函数的顶部范围内因此从主要开始到结束存在。因此在主要启动时按下B,在输入内部范围时按下A,在离开时按下弹出,然后在保留主要功能时弹出B.

答案 5 :(得分:1)

我的工作很大一部分是打击编译器,我不得不说他们并不总是做我们人类期望他们做的事情。即使您编写了编译器,您仍然会对结果感到惊讶,输入矩阵无法100%预测。

编译器的优化部分非常复杂,正如其他答案中所提到的,您观察到的可能是由于对设置的自愿响应,但它可能只是周围代码影响的结果,甚至在逻辑中没有这种优化。

无论如何,正如Micheal所说,你不应该依赖编译器来防止堆栈溢出,因为你可能只是将问题推迟到以后,当使用正常的代码维护或使用不同的输入集时,它将在管道中进一步崩溃,可能在用户手中。

答案 6 :(得分:-1)

在这种情况下,编译器别无选择。它不能假设printf()的任何特定行为。因此,只要A本身存在,就必须假设printf()可以挂在&A上。因此,A本身就存在于定义它的整个范围内。