临时变量会减慢我的程序速度吗?


73

假设我有以下C代码:

如果我要遍历多次,使用起来会更快int result = 5 + 10吗?我经常创建临时变量以使我的代码更具可读性,例如,如果两个变量是使用某个长表达式从某个数组中获取的,以计算索引。这在C语言中是不好的表现吗?那其他语言呢?


45
优化编译器会将代码更改为有效: int result = 15 ;
2501年

13
编译器将优化您的代码。将精力集中在诸如在循环中重复进行的(一部分)计算之类的事情会更有效率,而在循环开始之前,这样做会更好。
风向标

5
我认为他的意思是任何临时变量,即:正在使用a = b + c; d = a + e; 比使用a = b + c + d + e慢; 如果以编译器无法优化的方式完成操作,它可能会使用更多的内存,但是它不应该变慢。除非是商业和关键性能代码,否则最好的重点或工作效率。
异形2014年

1
@WeatherVane,尽管大多数编译器也会这样做,至少在某种程度上。总的来说,我认为最好专注于代码的可维护性,而不是像这样的微优化。
FireFly 2014年

6
@PeteBecker恐怕这不是有效的建议。尝试这样的事情并获得错误的印象是很容易的,因为您碰巧已经(或没有选择)一个案件,这是一般规则的例外。在没有清楚地了解编译器如何工作的情况下,仅测试几个案例就不能以任何方式使您相信所有案例都是正确的。进行这样的概括可能会非常冒险,并且经常会导致错误。
Jules 2014年

Answers:


84

一个现代的优化编译器应该将那些变量优化掉,例如,如果我们在godboltgcc使用以下示例并使用-std=c99 -O3标志(请参见live):

它将导致以下组装:

对于计算i + j,这是恒定传播的形式。

注意,我添加了,printf这样我们就有了副作用,否则func将被优化为:

as-if规则下允许进行这些优化,该规则仅要求编译器模拟程序的可观察行为。C99标准草案的5.1.2.3 程序执行部分对此进行了介绍,其中说:

在抽象机中,所有表达式均按语义指定的方式求值。如果实际实现可以推断出未使用表达式的值并且没有产生所需的副作用(包括由调用函数或访问易失性对象引起的副作用),则无需评估表达式的一部分。

另请参见:优化C ++代码:恒定折叠


2
如何查看我的C代码将生成什么程序集?
2014年

3
@REACHUS,gcc使用中gcc -S -o asm_output.s app.c
David Ranieri

2
我认为这不能解决问题。给出的示例很简单,但是我认为“如果这两个变量是使用某个长表达式从某个数组中获取来计算索引的那一部分”在这里更重要,那是无法优化的,还是可以吗?
Arturo TorresSánchez2014年

1
@ArturoTorresSánchez变量只是一个人类概念。没有真正的变量。编译器看到的是对表达式的引用,然后是对等的汇编代码。当有人问可变开销时,实际上意味着该引用的结果汇编指令的内存开销(例如寄存器/内存读/写)。“擦除”变量表示编译器是否足够聪明,可以识别出没有理由再次从内存中重新加载该“引用”,因为在这种情况下数据可以通过另一个引用来访问...(1)
Manu343726

1
@ArturoTorresSánchez...(在这种情况下为数组元素)。如今,编译器非常精通全局优化和表达式折叠(请参阅下面关于SSA格式的答案)。
Manu343726

29

对于优化的编译器来说,这是一项容易的任务。它将删除所有变量并替换result15

SSA形式的恒定折叠几乎是最基本的优化。


是的,但是由于“结果”在技术上是未使用的,因此程序将为空。
alexyorke

如果这是整个程序,则确实为空。如果不是,则将15“粘贴”到所有用法中。
usr 2014年

13

您给出的示例易于编译器优化。使用局部变量缓存从全局结构和数组中提取的值实际上可以加快代码的执行速度。例如,如果要从for循环内的复杂结构中获取某些内容,而编译器无法对其进行优化,并且您知道该值没有改变,则局部变量可以节省大量时间。

您可以使用GCC(也可以使用其他编译器)生成中间汇编代码,并查看编译器的实际操作。

这里讨论了如何打开程序集列表:使用GCC生成可读的程序集?

检查生成的代码并查看编译器的实际操作可能是有益的。


这是更有用的答案。其他的则进入事物的不断折叠部分,而不是进入存储位置点的本地副本。当编译器无法证明输入数组不重叠,或者某些未知函数无法对数组进行更改时,将全局变量和数组元素分配给局部变量通常会很有帮助。这通常使编译器无法多次重载相同的内存位置。
彼得·科德斯

10

尽管与代码的各种琐碎差异会以轻微改善或降低性能的方式干扰编译器的行为,但原则上,只要您不使用程序的含义,则是否使用此类临时变量都不会对性能造成任何影响改变了。一个好的编译器应该以任何一种方式生成相同或可比较的代码,除非您有意进行优化以使机器代码与源代码尽可能接近(例如出于调试目的)。


3
关键在于“只要不改变程序的含义”。在许多情况下,两种编写程序的方式在语义上会有细微的差别,这对程序员而言可能无关紧要,但将要求编译器为一种方法生成比另一种方法效率低得多的代码。
超级猫

1
正是supercat所说的:编译器不能总是证明两个指针/数组不重叠,或者函数调用不能改变内存的内容。因此,有时会迫使它在同一位置生成多个负载,但是使用int a = arr[i]会使编译器在函数调用和其他指针写入之间将值保存在寄存器中。
彼得·科德斯

我完全同意超级猫和彼得·科德斯的观点。
R .. GitHub停止帮助ICE

5

当您尝试学习编译器的功能时,您遇到的问题与我一样-您编写了一个简单的程序来演示该问题,并检查编译器的程序集输出,只是意识到编译器已经优化了所有内容您试图使它消失。您可能会发现main()中甚至一个相当复杂的操作都可以简化为:

您最初的问题不是“会发生什么int i = 5; int j = 10...?” 但是“临时变量通常会导致运行时损失吗?”

答案可能不是。但是您必须查看特定的非平凡代码的程序集输出。如果您的CPU有很多寄存器(如ARM),则i和j很可能位于寄存器中,就像这些寄存器直接存储函数的返回值一样。例如:

几乎可以肯定是与以下机器代码完全相同的机器代码:

我建议您使用临时变量,如果它们使代码更易于理解和维护,并且如果您确实想收紧循环,则无论如何都要研究汇编输出以找出如何提高性能的方法。可能。但是,如果没有必要,不要在几纳秒内牺牲可读性和可维护性。


解决方案是查看在编译器输出中是否有对参数进行运算的函数,而不是main()对编译时常量进行运算的函数。例如,下面是一个简单的示例,该示例求和一个浮点数组,并使用Godbolt的gcc asm输出: goo.gl/RxIFEF
Peter Cordes

我相信这就是我所说的:“您必须查看特定的非平凡代码的汇编输出”。;)
斯科特(Scott

我试图说的是,您可以在函数中添加一些琐碎的代码,并查看它的asm。(我认为这变成了对“琐碎的”定义的争论,这不是我想要的……)。您的示例使用函数调用生成ij。我的示例是makeijfunction参数,因此它们只是在函数代码开头的寄存器中坐在那里。
彼得·科德斯
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.