变量声明昂贵吗?


75

在用C进行编码时,遇到了以下情况。

考虑到if上面代码中的语句可以从函数中返回,我可以在两个地方声明变量。

  1. if声明之前。
  2. if声明。

作为一名程序员,我想在ifStatement之后保留变量声明。

申报处要收费吗?还是有其他原因更喜欢一种方法?


5
据我了解,分配堆栈空间只是增加堆栈指针。通过查看所有变量声明,可以在编译时确定函数需要多少堆栈空间。初始化变量是在运行时发生的,因此会影响性能。
Pieter Witvoet 2015年

3
查看为这两个选项生成的汇编代码-它应该为您提供明确的答案。只要确保您使用与发行版相同的编译器优化设置即可。
kestasx 2015年

67
如果您关心性能,那么应该有一些方法可以衡量应用程序的性能。如果您有能力衡量绩效,那么您可以回答自己的问题:尝试两种方法,您很快就会知道。如果您没有任何衡量绩效的方法,但您关心绩效,那么您的问题应该是“如何设置工具来衡量绩效?”
埃里克·利珀特

1
你能用C做到吗?
Salman A

2
@ g24l:大多数问题可以用相同的方式回答的事实说明了为什么这些问题不适合本网站。就在那儿,上面写着“这段代码做什么?” 问题; 您编写了代码,运行了一下,看看它做了什么,然后您就会知道。
埃里克·利珀特

Answers:


97

在C99和更高版本(或带有C89的通用扩展名)中,您可以随意混合使用语句和声明。

就像在早期版本中一样(只有这样才能使编译器变得更加智能和更具侵略性),编译器会决定如何分配寄存器和堆栈,或执行符合规则的任何其他优化。
这意味着在性能方面,不会有任何差异。

无论如何,这不是允许这样做的原因:

这是为了限制范围,从而减少了人们在解释和验证代码时必须牢记的上下文


1
感谢您的回覆,也感谢所有人。如果我正确理解您的声明,那么它不会带来任何性能问题,但是会带来清晰度。是对的吗 ?
Whoami

5
如果要查看优化器的运行情况,请编写一个带有类似初始化的声明的函数,例如char *foo = "something",使用优化器和调试标志编译代码,然后gcc -O3 -g在调试器中逐步执行该函数。步进点将反弹并延迟初始化变量,直到需要它为止。
Schwern 2015年

5
+1注意到减少此处的脑力开销的重要性...许多人对此遗忘了。

44

做任何有意义的事情,但是当前的编码风格建议将变量声明尽可能地接近其用法

实际上,在第一个编译器之后,几乎每个编译器都可以使用变量声明。这是因为实际上所有处理器都使用堆栈指针(可能还有帧指针)来管理其堆栈。例如,考虑两个功能:

如果我要在现代的编译器上编译它们(并告诉它不是很聪明并优化我未使用的局部变量),我会看到这个(x64汇编示例。其他类似):

注意:这两个函数具有相同数量的操作码!

这是因为实际上,所有编译器都会预先分配所需的所有空间(除非有别的东西需要alloca单独处理)。实际上,在x64上,必须以这种有效方式这样做。

(编辑:正如Forss所指出的,编译器可能会将一些局部变量优化到寄存器中。从技术上讲,我应该争论的是,第一个“溢出”到堆栈中的变量需要2个操作码,而其余的则是免费的)

出于相同的原因,编译器将收集所有局部变量声明,并为它们分配空间。 C89要求所有声明都是预先声明的,因为它被设计为1遍编译器。为了使C89编译器知道要分配多少空间,它需要在发出其余代码之前先了解所有变量。在现代语言(如C99和C ++)中,期望编译器比1972年的智能得多,因此,此限制放宽了对开发人员的便利。

现代编码实践建议使变量接近其用法

这与编译器无关(编译器显然不在乎一种或另一种方式)。已经发现,如果将变量放在使用它们的位置附近,大多数人类程序员会更好地阅读代码。这只是一个样式指南,请随时不同意它,但是开发人员之间有一个显着的共识,那就是这是“正确的方法”。

现在来看一些极端情况:

  • 如果将C ++与构造函数一起使用,则编译器将预先分配空间(因为这样做的速度更快,并且不会受到损害)。但是,在代码流中的正确位置之前,不会在该空间中构造变量。在某些情况下,这意味着将变量放置在最接近它们的位置使用的速度甚至比放置在它们前面的速度更快...流控制可能将我们引向变量声明的周围,在这种情况下,甚至不需要调用构造函数。
  • alloca在此之上的一层上处理。对于那些好奇的人,alloca实现往往会产生将堆栈指针向下移动任意数量的效果。alloca需要使用函数来以一种或另一种方式跟踪此空间,并确保堆栈指针在离开前向上重新调整。
  • 在某些情况下,您通常需要16字节的堆栈空间,但是在一种情况下,您需要分配50kB的本地数组。无论将变量放在代码中的什么位置,实际上每次调用该函数时,所有编译器都会分配50kB + 16B的堆栈空间。这很少有关系,但是在强迫性递归代码中,这可能会使堆栈溢出。您必须将使用50kB阵列的代码移到其自己的函数中,或使用alloca
  • 如果您分配的内存空间超过一页的堆栈空间,则某些平台(例如Windows)需要在序言中进行特殊的函数调用。这根本不应该改变任何分析(在实现中,这是一个非常快速的叶子函数,每页仅戳一个单词)。

请明确指出C89,ISO C90,K&R C等。“因为它被设计为1遍编译器,所以要求所有声明都是预先声明的。” 您显然知道差异,因为您将C99称为例外,但C!= C89。可以说,现在,C = C11。
杰夫·哈蒙德

还请注意,只要范围界定有效,就可以使用{}来混合C89中的声明和代码。
Jeff Hammond 2015年

@Jeff:啊,我不知道您可以用这种方式使用括号。猜猜这就是我成为一名尝试使用C术语回答的C ++人所得到的!
Cort Ammon

C ++是C89的超集,因此大多数时候您应该没事。C99会让您满意的,因为它不是C ++ 03的严格子集。
Jeff Hammond

“实际上,在第一个编译器之后,几乎每个编译器都可以使用变量声明。” 如果您考虑使用比编译器可以优化到寄存器中更多的变量,这是真的吗?
Forss 2015年

21

在C语言中,我相信所有变量声明都像在函数声明的顶部一样被应用。如果在一个块中声明它们,我认为这只是一个范围界定的事情(我认为在C ++中是不一样的)。编译器将对变量执行所有优化,有些甚至可能在较高的优化中有效地消失在机器代码中。然后,编译器将决定变量需要多少空间,然后在执行过程中,创建一个称为变量的堆栈,该变量将驻留在其中。

调用一个函数时,该函数使用的所有变量以及有关被调用函数的信息(即返回地址,参数等)都将被放入堆栈中。声明变量的位置无关紧要声明变量就无关紧要了,无论如何它都会分配到堆栈中。

声明变量本身并不“昂贵”。如果很容易不被用作变量,则编译器可能会将其删除为变量。

看一下这个:

大栈

维基百科上的调用堆栈堆栈上的其他一些地方

当然,所有这些都是与实现和系统有关的。


VLA如何使用?还是完全消除了变量/它的堆栈空间?还是别名插槽?
Deduplicator 2015年

@Deduplicator显然,它增加了堆栈帧大小(根据演示文稿)。它说alloca,但是两者是联系在一起的alloca从堆栈中分配空间。提醒:这些是实现定义的。
杰里米·罗迪

只是想用一些构造来刺探您,您的解释现在才可以排除,因此您可以对其进行完善。
Deduplicator 2015年

@Deduplicator对不起?我不明白您要说的是什么。
杰里米·罗迪

6
@Paul如果愿意,可以将堆栈“反转”绘制(在纸的顶部位于纸张的顶部,如果您手动绘制则具有明显的优势),这种情况并不罕见。在任何情况下,您可能都想告诉HP他们多年来一直做错了事(PA-RISC传统上会使堆栈向上增长;)如果您没有pushpop操作,那么会很麻烦(实际上有没有任何RISC ISA?)这只是一个惯例-MULTICS的堆栈数量不断增长。
Voo,2015年

12

是的,这可能会导致清晰度下降。如果存在某种情况下函数在某种情况下根本不执行任何操作的情况(例如,在您遇到的情况下,当找到全局false时),那么将支票放在顶部(在上面显示该支票的位置)肯定更容易理解-在调试和/或记录文档时必不可少的东西。


11

它最终取决于编译器,但通常所有本地变量都在函数的开头分配。

但是,分配局部变量的成本非常小,因为它们被放入堆栈中(或在优化后放入寄存器中)。


“在功能开始时分配所有本地人”。你的意思是说如果我也使用惰性方法,它会分配吗?
Whoami

可能是。这取决于编译器的操作方式,但是最简单的方法是在开始时分配全部,然后在结束时分配所有。
头脑风暴

2
但是,分配只是一条添加指令,因此非常便宜。根本不像使用malloc。
头脑风暴

但是,从所有其他工程师的答复来看,除了可读性之外,似乎没有任何性能或其他任何形式的支出吗?:)
Whoami'1

7

最佳实践是采用一种惰性方法,即仅在确实需要它们时才声明它们;)(而不是在之前)。它带来以下好处:

如果将这些变量声明为尽可能靠近使用位置,则代码更具可读性。


4
这是不正确的(至少对我而言)。我发现,如果所有变量都在函数顶部的块中声明,而不是分散在代码中,则代码更具可读性(并且可编辑-我不必遍历代码来更改声明)。正如其他人指出的那样,编译器足够聪明,可以优化分配。
jamesqf

2
对你来说,也许 但不适用于大多数编码社区!
CinCout 2015年

3
这是不正确的时期。该示例使用在堆栈上静态分配的变量。无论这些局部变量在函数中的声明位置如何,编译器都会生成一条指令以在堆栈上为这些局部变量保留内存。
Mark E. Haase 2015年

@binaryBaBa第二点“您最终没有在较早阶段为变量分配内存”与代码的可读性无关,正如其他人已经提到的那样,我认为这是不正确的。
TJ

是的,考虑到当今改进的编译器,这对我来说很有意义。进行了相应的编辑。
CinCout 2015年

6

声明应尽可能靠近使用处。理想情况是在嵌套块中。因此,在这种情况下,在if语句上方声明变量是没有意义的。


5

如果你有这个

那么保留给它的堆栈空间foosomecondition可以明显地重复使用str1等等,因此,通过在之后声明if,可以节省堆栈空间。根据编译器的优化能力,如果您通过移除一对内括号来使功能变平,或者如果您确实在;之前声明了etc等,那么也可能会节省堆栈空间。但是,这要求编译器/优化器注意范围不会“真正”重叠。通过在之后放置声明,即使没有优化也可以简化此行为-更不用说提高了代码的可读性。str1ifif


5

每当您在C范围(例如函数)中分配局部变量时,它们就没有默认的初始化代码(例如C ++构造函数)。并且由于它们不是动态分配的(它们只是未初始化的指针),因此无需调用其他(可能是昂贵的)函数malloc来准备/分配它们。

由于堆栈的工作方式,分配堆栈变量仅意味着递减堆栈指针(即增加堆栈大小,因为在大多数体系结构中,其向下增长),以便为其腾出空间。从CPU的角度来看,这意味着执行一条简单的SUB指令:(SUB rsp, 4如果您的变量为4字节大-例如常规的32位整数)。

此外,当声明多个变量时,编译器足够聪明,可以将它们实际组合在一起成为一条大SUB rsp, XX指令,这XX是作用域局部变量的总大小。理论上。实际上,情况有所不同。

在这种情况下,当我发现(非常容易)发现编译器“幕后”发生了什么时,我发现GCC Explorer是一个非常有价值的工具。

因此,让我们看一下实际编写如下函数时会发生什么:GCC explorer link

C代码

结果组装

事实证明,海湾合作委员会甚至比这更聪明。它甚至根本不执行SUB指令来分配局部变量。它只是(内部)假设该空间已“被占用”,但未添加任何指令来更新堆栈指针(例如SUB rsp, XX)。这意味着堆栈指针不会保持最新,但由于在这种情况下,在使用堆栈空间后PUSH不再执行任何指令(并且不会进行rsp相对查找),因此没有问题。

这是一个没有声明其他变量的示例:http : //goo.gl/3TV4hE

C代码

结果组装

如果您在过早返回之前查看代码(jmp .L3,跳转到清除并返回代码),则不会调用其他指令来“准备”堆栈变量。唯一的区别是,存储在ediesi寄存器中的功能参数a和b以比第一个示例([rbp-4][rbp - 8])高的地址加载到堆栈上。这是因为没有像第一个示例中那样为本地变量“分配”额外的空间。因此,如您所见,添加这些局部变量的唯一“开销”是减法项的更改(即,甚至不添加其他减法运算)。

因此,在您的情况下,仅声明堆栈变量几乎没有任何成本。


4

除了记录我们为什么这样做时,我更喜欢在函数顶部保留“尽早”状态。如果我们将其放在一堆变量声明之后,那么不熟悉代码的人可能会容易地错过它,除非他们知道必须寻找它。

仅记录“提前”条件并不总是足够的,最好在代码中也清楚说明。将早期退出条件放在顶部也可以使文档与代码保持同步更加容易,例如,如果我们稍后决定删除早期退出条件,或添加更多此类条件。


4

如果确实很重要,那么避免分配变量的唯一方法可能是:

但实际上,我认为您不会发现性能方面的好处。如果有什么开销的话。

当然,如果您正在编写C ++,并且其中一些局部变量具有非平凡的构造函数,则可能需要将它们放在检查之后。但是即使那样,我仍然认为拆分功能将无济于事。


1

如果在if语句之后声明变量并立即从函数返回,则编译器不会在堆栈中分配内存。


@ThomasPapamilhos:不是。答案实际上是不确定的,但以我的经验,大多数编译器将分配完整的空间以在入口时完成该功能,以避免堆栈指针重复移动而几乎得不到任何好处。
2015年
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.