在循环中声明变量是否高效?

5
如果我执行以下任一操作(我假设这些操作在本问题中是等效的):
for(int i=0; i<A; i++)
{
  //... do stuff
  for(int j=0; j<B; j++)
  {
    //... do stuff
  }
  //... do stuff
}

for(int i=0; i<A; i++)
{
  int j;
  //... do stuff
}

变量 j 在每次循环中是否会在堆栈上重新创建(即 SP 是否在每次循环中不断更新),还是编译器聪明到足以知道函数可能同时具有多少个局部变量,然后在函数入口为所有这些变量腾出空间?
我知道这理论上取决于编译器,但我假设像这样的简单问题在所有主要编译器中都很常见。如果不是这样,有人知道特别是关于 GCCVC++ 编译器吗?

4
你可以查看使用不同优化级别生成的汇编代码。很可能变量只会被创建一次。 - juanchopanza
通常会被优化掉。不必在外部声明它。这是不必要的。 - dchhetri
4个回答

9

这是一个高效的方法。像现代大多数编译器一样,gcc将对其进行优化。此外,请记住Donald Knuth所说的:

大部分时间我们应该忘记小细节:过早的优化是万恶之源。

你可以通过比较汇编代码来检查它是否有效。例如,可以使用diff进行比较。要生成汇编,请使用-S标志,即gcc -S。这相当于gcc -S -O0,因为默认优化级别为0。因此,即使在最低级别优化下,gcc也会为您处理此变量。

首选阅读方式的第一版:

#include <stdio.h>

int main()
{
    for (int i = 0; i < 10; i++)
    {
        for (int j = 0; j < 10; j++)
        {
            printf("%d ", i + j);
        }
    }
    return 0;
}

第二版:
#include <stdio.h>

int main()
{
    for (int i = 0; i < 10; i++)
    {
        int j;
        for (j = 0; j < 10; j++)
        {
            printf("%d ", i + j);
        }
    }
    return 0;
}

相同的组装结果:

    .file   "main.cpp"
    .section    .rodata
.LC0:
    .string "%d "
    .text
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    pushq   %rbp
    .cfi_def_cfa_offset 16
    movq    %rsp, %rbp
    .cfi_offset 6, -16
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    $0, -4(%rbp)
    jmp .L2
.L5:
    movl    $0, -8(%rbp)
    jmp .L3
.L4:
    movl    -8(%rbp), %eax
    movl    -4(%rbp), %edx
    leal    (%rdx,%rax), %eax
    movl    %eax, %esi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    printf
    addl    $1, -8(%rbp)
.L3:
    cmpl    $9, -8(%rbp)
    setle   %al
    testb   %al, %al
    jne .L4
    addl    $1, -4(%rbp)
.L2:
    cmpl    $9, -4(%rbp)
    setle   %al
    testb   %al, %al
    jne .L5
    movl    $0, %eax
    leave
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5.1) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

int i-4(%rbp)int j-8(%rbp)。正如您所看到的,int j 没有被重新分配或其他操作。


2
如果你遵循优化的“不要基于不具代表性的测试用例做出概括”的规则,那么你引用 Knuth 的话语会更加严肃。你嵌套的 for 循环没有效果,任何值得一试的编译器都会将 f() 的两个版本优化为 return 0;,但这可能意味着编译器在循环不是微不足道的情况下并不总是生成相同的代码。 - Pascal Cuoq
它并不是为了仅仅返回0而进行优化的,但我理解你的意思。我进行了编辑,谢谢。汇编代码仍然是一样的。 - Adam Stelmaszczyk

1
我相信变量只会被创建一次,虽然我不在意这个问题,而且我认为你也不应该在意。
这很可能是你进行预优化(或不必要的优化)的一个例子;在循环内声明变量所产生的潜在低效性非常小,通过在不同位置声明变量来“优化”代码对程序整体运行时和内存使用的影响可以忽略不计。
考虑花时间优化算法并找到高效的数据结构,因为这可能是更好地利用你的时间。

从某种意义上说,这是一个关于编译器而不是代码优化的问题。我并不真正关心这个问题,只是好奇。 - Baruch
很抱歉,但“我相信”和“我不在乎”都不能作为答案的合格选项。这个问题是有效的,值得更详细地阐述一下。也许,虽然不太可能,但这个循环可能是某台超级计算机花费大部分时间的循环。 - johannes_lalala

0

为什么不自己试一试呢?

class foo {
public:
    foo () { std::cout << "Construct\n"; }
    ~foo () { std::cout << "Destruct\n"; }
    };

int main () {
    for ( int i = 0; i < 10; ++i ) {
        foo f;
    }

    return 0;
}

你会发现每次循环都会调用f的构造函数(和析构函数!)。所以你的问题的答案是:“是的,变量在每次循环中都会重新创建。”

回到你的例子,你声明了一个int,它有一个不做任何事情的构造函数和析构函数。因此,在循环内部声明一个int不会有性能损失。


4
构造函数和析构函数是与堆栈调整无关的问题。 - aschepler
2
这与内置类型不同,因为内置类型没有隐式构造函数/析构函数或初始化。 - Robert S. Barnes
我在询问关于堆栈调整的问题。通过放置new,可以在不进行堆栈调整的情况下调用构造函数,这也是每次对象构造时发生的事情(内存被分配,然后为该内存调用构造函数)。 - Baruch
好的。抱歉造成困惑。栈调整非常便宜,只需要改变寄存器中的值即可。 - Marshall Clow

0
编译器现在已经太强大了,可以优化这些东西。所以不要费心,按照您的方便使用即可。过早地优化更加有害。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接