在循环内声明变量会产生额外开销吗? (C++)

169

我只是想知道,如果你这样做,是否会损失速度或效率:

int i = 0;
while(i < 100)
{
    int var = 4;
    i++;
}

这段代码声明了int var变量一百次。我认为应该可以这样做,但我不能确定。相比之下,以下方法是否更加实用/快速:

int i = 0;
int var;
while(i < 100)
{
    var = 4;
    i++;
}

它们在速度和效率方面是相同的吗?


7
请注意,上述代码并没有“声明”100个变量,但代码含义不变。 - jason
1
@Rabarberski:所提到的问题并不是完全重复,因为它没有指定语言。这个问题是关于C++的。但根据您提到的问题的答案,答案取决于语言和可能的编译器。 - DavidRR
2
@jason 如果第一个代码片段没有声明变量'var'一百次,你能解释一下发生了什么吗?它只是声明变量一次并初始化100次吗?我本来以为代码会声明和初始化变量100次,因为循环中的所有内容都会执行100次。谢谢。 - randomUser47534
1
这个回答解决了你的问题吗?在循环内部声明变量是好的实践还是不好的实践? - user202729
13个回答

202

局部变量的堆栈空间通常在函数作用域中分配。因此,在循环内部不会进行堆栈指针调整,只是将4赋值给var。因此,这两个代码片段具有相同的开销。


54
我希望我们学院的那些教授至少了解这个基本知识。有一次,我在循环内部声明变量,他笑话我做法不对,理由是会降低性能,我当时非常疑惑,感到很不可思议。 - Mehrdad Afshari
21
你确定需要立即谈论堆栈空间吗?这样的变量也可能在寄存器中。 - toto
5
像这样的变量有可能是“无处不在”的——var变量被初始化了,但从未被使用,因此一个合理的优化器可以完全删除它(除非在循环后面的第二个代码片段中该变量被使用)。 - CiaPan
1
@Mehrdad Afshari 循环中的变量在每次迭代时都会调用其构造函数。编辑-我看到您在下面提到了这一点,但我认为它也值得在被接受的答案中提及。 - hoodaticus

115

对于原始类型和POD类型,这没有任何区别。在两种情况下,编译器将在函数开始时为变量分配堆栈空间,并在函数返回时释放它。

对于具有非平凡构造函数的非POD类类型,情况就不同了——在这种情况下,将变量放在循环外只会在每次迭代时调用一次构造函数和析构函数以及赋值运算符,而将其放在循环内将在每次循环迭代中调用构造函数和析构函数。根据类的构造函数、析构函数和赋值运算符的具体实现,这可能是需要或不需要的。


这似乎不是真的。如果您在循环中使用非POD类型并反复赋值,就像他的示例一样,那么对新变量的赋值也会一遍又一遍地调用构造函数。 - Brian
43
正确的想法但是理由错误。循环外的变量。构造一次,销毁一次,但在每次循环中应用赋值运算符。循环内的变量。每次迭代都会应用构造/析构函数,但不执行任何赋值操作。 - Martin York
9
这是最佳答案,但这些评论令人困惑。调用构造函数和赋值运算符之间有很大的区别。 - Andrew Grant
1
如果循环体无论如何都执行赋值操作,那么这是正确的。如果只有独立于循环体且不变的初始化,优化器可以将其提升。 - peterchen
7
为什么赋值运算符通常被定义为将对象进行复制构造,接着交换指针(以确保异常安全),最后销毁临时变量?因此,赋值运算符与上述构造/销毁周期并没有太大的不同。具体实例可以参考https://dev59.com/BnVC5IYBdhLWcg3wjx5d#255744中典型的赋值运算符。 - Martin York
1
如果构造/析构函数代价昂贵,它们的总代价是运算符=代价的合理上限。但实际上赋值可能会更便宜。此外,随着我们将这个讨论从int扩展到C++类型,可以将'var=4'泛化为不同于“从相同类型的值分配变量”的其他操作。 - greggo

70

它们是完全相同的,这里是如何查找编辑器产生的代码以确定(即使未设置高度优化):

查看编译器(gcc 4.0)对简单示例的处理:

1.c:

main(){ int var; while(int i < 100) { var = 4; } }

使用gcc -S 1.c命令来生成汇编代码文件1.s:

1.s:

_main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $24, %esp
    movl    $0, -16(%ebp)
    jmp L2
L3:
    movl    $4, -12(%ebp)
L2:
    cmpl    $99, -16(%ebp)
    jle L3
    leave
    ret

2.c

main() { while(int i < 100) { int var = 4; } }

使用命令“gcc -S 2.c”

会生成一个名为“2.s”的文件。

_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $24, %esp
        movl    $0, -16(%ebp)
        jmp     L2
L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3
        leave
        ret

从这些中,你可以看到两件事情:首先,代码在两个地方是一样的。

其次,变量 var 的存储空间是在循环外分配的:

         subl    $24, %esp

最后,循环中唯一的事情就是赋值和条件检查:

L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3

这大概是在不完全删除循环的情况下你能实现的最高效率了。


2
这几乎是在不完全移除循环的情况下,你能够达到的最高效率了。但事实并非如此。部分展开循环(例如每次执行4次)将极大地加快速度。还有许多其他优化方法...尽管现代大多数编译器可能会意识到根本没有必要进行循环。如果'i'稍后被使用,它只会简单地将'i'设置为100。 - darron
就像原始帖子一样! - Alex Brown
2
我喜欢那些用证据支持理论的答案!很高兴看到 ASM 转储支持相等代码理论的证明。+1 - Xavi Montero
抱歉,但是当您提供的代码中没有递增变量i的语句时,您实际上是如何产生结果的?现代编译器是否足够智能,可以确定每次迭代时希望向前或向后递增多少? - Nicholas Hamilton
1
我通过为每个版本生成机器代码来产生结果,无需运行它。 - Alex Brown
显示剩余3条评论

14

现在最好在循环内部声明变量,除非它是一个常数,因为编译器能够更好地优化代码(减少变量作用域)。

编辑:这个答案现在大多已经过时。随着后现代编译器的兴起,编译器无法解决的情况越来越少了。我仍然可以构造出这种情况,但大多数人会将其分类为糟糕的代码。


4
我怀疑这不会影响优化——如果编译器执行任何形式的数据流分析,它可以找出在循环外部未被修改,因此应该在两种情况下生成相同的优化代码。 - Adam Rosenfield
3
如果你有两个不同的循环使用相同的临时变量名称,它就无法解决问题。 - Joshua

11

大多数现代编译器都会为您进行优化。尽管如此,我会使用您的第一个示例,因为我认为它更易读。


3
我不认为这算是优化,因为它们是局部变量,堆栈空间只是在函数开始时分配。没有真正的“创建”过程会影响性能(除非调用构造函数,那就完全是另外一回事)。 - Mehrdad Afshari
你说得对,“优化”这个词不太准确,但我却找不到更好的词来表达。 - Andrew Hare
问题在于这样的优化器将使用活跃范围分析,而两个变量都相当死。 - MSalters
编译器进行数据流分析后,它们之间没有任何区别。个人而言,我更喜欢变量的作用域应该限制在使用它的范围内,这不是为了提高效率,而是为了清晰明了。 - greggo

9

对于内置类型,两种风格可能没有区别(甚至生成的代码都相同)。

然而,如果变量是带有非平凡构造/析构函数的类,则运行时成本可能会有很大差异。通常我会将变量限定在循环内部(尽可能缩小作用域),但如果发现具有性能影响,我会考虑将类变量移出循环的作用域。但是,这需要进行额外的分析,因为代码路径的语义可能会改变,因此只有在语义允许的情况下才能这样做。

RAII类可能需要这种行为。例如,管理文件访问生命周期的类可能需要在每个循环迭代中创建和销毁以正确管理文件访问。

假设您有一个 LockMgr 类,它在构造时获取关键部分并在销毁时释放关键部分:

while (i< 100) {
    LockMgr lock( myCriticalSection); // acquires a critical section at start of
                                      //    each loop iteration

    // do stuff...

}   // critical section is released at end of each loop iteration

与之相当不同:

LockMgr lock( myCriticalSection);
while (i< 100) {

    // do stuff...

}

7

这两个循环的效率是相同的。它们都需要无限长的时间 :) 建议在循环内部增加 i 的值。


啊,是的,我忘记了提及空间效率 - 没关系 - 两个整数都可以。对我来说,程序员们只看到树木,而忽略了森林 - 所有这些建议都关于一些不终止的代码,这对我来说似乎很奇怪。 - Larry Watanabe
如果它们不终止也没关系,它们都没有被调用。 :-) - Nosredna

2
#include <stdio.h>
int main()
{
    for(int i = 0; i < 10; i++)
    {
        int test;
        if(i == 0)
            test = 100;
        printf("%d\n", test);
    }
}

上面的代码总是打印出100 10次,这意味着循环内的局部变量只在每个函数调用时分配一次。


2

我曾经进行一些性能测试,令我惊讶的是,情况1实际上更快!我想这可能是因为在循环内声明变量会减少其范围,因此它会更早地被释放。但是,那是很久以前,在一个非常老的编译器上。我相信现代编译器会更好地优化掉差异,但仍然不妨将变量范围尽可能缩短。


区别可能来自范围的不同。范围越小,编译器消除变量序列化的可能性就越大。在小循环范围内,变量可能被放置在寄存器中,而不是保存在堆栈帧上。如果您在循环中调用函数或解引用指针,则编译器无法确定指针指向何处,如果它在函数范围内,则会溢出循环变量(指针可能包含“&i”)。 - Patrick Schlüter
请发布您的设置和结果。 - jxramos

0

确定的方法是计时它们。但如果有差异,那么差异将是微不足道的,因此您需要一个非常大的计时循环。

更重要的是,第一种方式是更好的风格,因为它初始化了变量var,而另一种方式则未对其进行初始化。这和指导方针一致,即应尽可能靠近使用点定义变量,这意味着通常应优先选择第一种形式。


唯一确定的方法是对它们进行计时。-1 不正确。抱歉,但另一篇帖子通过比较生成的机器语言并发现其基本相同证明了这一点。我对你的回答没有任何问题,但是 -1 的用法不正确吗? - Bill K
检查发出的代码确实很有用,在像这样的简单情况下可能已经足够。然而,在更复杂的情况下,像引用位置等问题会出现,只能通过计时执行来测试这些问题。 - anon

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