在循环中声明变量是否有开销?(C ++)


158

我只是想知道如果您执行以下操作是否会降低速度或效率:

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

声明int var一百次。在我看来,好像会有,但我不确定。这样做会更实用/更快吗?

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

还是在速度和效率上都一样?


7
需要明确的是,上面的代码不会“声明” var一百次。
杰森

1
@Rabarberski:所引用的问题并不完全相同,因为它未指定语言。这个问题是C ++特有的。但是根据发布给您所参考问题的答案,答案取决于语言,甚至取决于编译器。
DavidRR

2
@jason如果第一段代码没有将变量'var'声明一百次,您能解释发生了什么吗?它是否只声明一次变量并将其初始化100次?我本以为代码会声明并初始化变量100次,因为循环中的所有内容都会执行100次。谢谢。
randomUser47534 2015年

Answers:


194

局部变量的堆栈空间通常在函数范围内分配。因此,循环内不会发生堆栈指针调整,只需将4分配给即可var。因此,这两个摘要具有相同的开销。


50
我希望那些在大学里教书的人至少知道这一基本知识。一旦他嘲笑我在循环内声明一个变量,我想知道是怎么回事,直到他引用性能作为不这样做的原因,而我就像“ WTF !?”。
mmx

18
您确定要立即谈论堆栈空间吗?这样的变量也可以在寄存器中。
toto

3
@toto这样的变量也可能无处var变量已初始化但从未使用过,因此合理的优化程序可以将其完全删除(如果变量在循环后的某个地方使用,则第二个片段除外)。
CiaPan

@Mehrdad Afshari循环中的变量在每次迭代中都会调用一次其构造函数。编辑-我看到你在下面提到了这一点,但我认为它也应该在公认的答案中提及。
hoodaticus '17

106

对于基本类型和POD类型,这没有什么区别。两种情况下,编译器都会在函数的开头为变量分配堆栈空间,并在函数返回时取消分配。

对于具有非平凡构造函数的非POD类类型,它将有所作为-在这种情况下,将变量放在循环外只会调用一次构造函数和析构函数,而每次迭代都会调用赋值运算符,而将其放入循环将为循环的每次迭代调用构造函数和析构函数。根据类的构造函数,析构函数和赋值运算符的作用,这可能是理想的,也可能不是理想的。


42
纠正想法错误的原因。循环外的变量。构造一次,销毁一次,但赋值运算符应用每次迭代。循环内变量。构造器/解构器每次迭代都重复使用,但赋值操作为零。
马丁·约克

8
这是最好的答案,但这些评论令人困惑。调用构造函数和赋值运算符有很大的不同。
Andrew Grant

1
如果循环主体无论如何都执行赋值,而不仅仅是初始化,这事实。而且,如果仅存在与主体无关的/恒定的初始化,则优化程序可以将其提升。
peterchen 2009年

7
@安德鲁·格兰特:为什么。赋值运算符通常定义为复制到tmp的构造,然后进行交换(以确保异常安全),然后销毁tmp。因此,赋值运算符与上面的构造/破坏周期没有什么不同。有关典型赋值运算符的示例,请参见stackoverflow.com/questions/255612/…
马丁·约克

1
如果构造/销毁很昂贵,则它们的总成本是操作员成本的合理上限。但是任务确实可以便宜一些。另外,随着我​​们将讨论从int扩展到C ++类型,人们可以将'var = 4'推广为除'从相同类型的值分配变量'以外的其他操作。
greggo 2014年

69

它们都是相同的,这是通过查看编译器的工作(即使没有将优化设置为高)来找出的方法:

看看编译器(gcc 4.0)对您的简单示例做了什么:

1.c:

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

gcc -S 1.c

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

假设代码完全更改为递增的“ i”,因为这只是一个永久循环。
darron

就像原始帖子一样!
亚历克斯·布朗

2
我喜欢有证据支持的理论答案!很高兴看到ASM转储支持相等代码的理论。+1
哈维·蒙特罗

1
我实际上是通过为每个版本生成机器代码来产生结果的。无需运行它。
亚历克斯·布朗

14

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

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


4
我怀疑这会影响优化-如果编译器执行任何形式的数据流分析,它会发现没有在循环外对其进行修改,因此在两种情况下它都应生成相同的优化代码。
亚当·罗森菲尔德2009年

3
但是,如果您使用相同的临时变量名称进行了两个不同的循环,则不会弄清楚。
约书亚

11

大多数现代编译器都会为您优化此过程。话虽这么说,我会使用您的第一个示例,因为我觉得它更具可读性。


3
我并不真的认为它是一种优化。由于它们是局部变量,因此堆栈空间仅在函数开始时分配。没有任何真正的“创造”会影响性能(除非调用了构造函数,这完全是另一回事)。
mmx

没错,“优化”这个词是错误的,但我不知所措。
Andrew Hare

问题是这样的优化器将使用有效范围分析,并且两个变量都已失效。
MSalters 2009年

那么“一旦编译器进行数据流分析,它们之间就不会看到任何区别”。就我个人而言,我更喜欢将变量的范围限制在使用位置,而不是为了提高效率,而是为了清楚起见。
greggo 2014年

9

对于内置类型,这两种样式之间可能没有区别(可能直接到生成的代码)。

但是,如果变量是具有非平凡的构造函数/析构函数的类,则运行时成本可能会有很大的不同。我通常将变量的作用域放在循环内部(以使作用域尽可能小),但是,如果结果证明对性能产生影响,我希望将类变量移到循环作用域之外。但是,这样做需要进行一些额外的分析,因为ode路径的语义可能会更改,因此只有在语义允许的情况下才能执行此操作。

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...

}

6

两个回路具有相同的效率。它们都将花费无限的时间:)在循环内增加i可能是一个好主意。


嗯,是的,我忘了解决空间效率问题-没关系-两者均为2 int。令我感到奇怪的是,程序员缺少树的森林-所有这些关于某些代码不会终止的建议。
拉里·渡边

如果他们不终止也可以。他们两个都没有被叫。:-)
Nosredna,2009年

2

我曾经进行过一些性能测试,令我惊讶的是,发现情况1实际上更快!我想这可能是因为在循环内声明变量会减小其范围,因此它会较早释放。但是,那是很久以前的,使用的是非常旧的编译器。我相信现代编译器在优化差异方面做得更好,但是将变量范围保持在尽可能短的范围内也无害。


差异可能来自范围上的差异。范围越小,编译器越有可能消除变量的序列化。在小循环范围内,该变量可能已放入寄存器中,而没有保存在堆栈帧中。如果您在循环中调用函数,或取消引用指针,则编译器并不真正知道其指向的位置,如果它在函数范围内(指针可能包含&i),则会溢出循环变量。
PatrickSchlüter'15

请发布您的设置和结果。
jxramos

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次,这意味着循环内的局部变量在每个函数调用中仅分配一次。


0

确保唯一的方法是计时它们。但是,如果存在差异,那么差异将是微观的,因此您将需要强大的大时序循环。

更重要的是,第一个是更好的样式,因为它会初始化变量var,而另一个则使变量未初始化。这以及人们应该尽可能在变量的使用点附近定义变量的准则,这意味着通常应该首选第一种形式。


“唯一确定的方法就是计时。” -1不正确。抱歉,但另一篇文章通过比较生成的机器语言并发现其本质上相同,证明了这一错误。总的来说,我的问题没有任何问题,但是-1代表什么是正确的吗?
比尔K 2009年

检查发出的代码肯定是有用的,并且在这种简单情况下就足够了。但是,在更复杂的情况下,诸如参考点位置之类的问题会浮出水面,并且只能通过定时执行来测试这些问题。

-1

只有两个变量,编译器很可能会为两个变量分配一个寄存器。这些寄存器仍然存在,因此不需要时间。两种情况下都有2个寄存器写指令和1个寄存器读指令。


-2

我认为大多数答案都缺少要考虑的主要观点:“很清楚”,显然在所有讨论中事实都是如此;不它不是。我建议在大多数循环代码中,效率几乎是没有问题的(除非您为火星着陆器进行计算),所以实际上唯一的问题是什么看起来更明智,可读和可维护-在这种情况下,我建议声明循环前和循环外的变量-这使它更清晰。然后像您和我这样的人甚至都不会浪费时间在网上检查它是否有效。


-6

事实并非如此,但开销却很小。

即使它们可能最终会在堆栈中的同一位置,它仍然会对其进行分配。它将为该int分配堆栈上的内存位置,然后在}末尾释放它。从某种意义上讲,它不是从无堆的意义上将sp(堆栈指针)移动1。在您的情况下,考虑到它只有一个局部变量,它将仅等同于fp(帧指针)和sp

简短的答案是:不要以几乎相同的方式工作。

但是,尝试阅读有关堆栈组织方式的更多信息。我的本科学校对此有很好的演讲


同样,-1不正确。阅读查看程序集的帖子。
比尔K 2009年

不,你错了。看一下用该代码生成的汇编代码
grobartn
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.