在C语言程序开发中,有时为了一行代码不至于过长,或者其他原因,程序员常常会使用一些临时的中间变量,使用临时中间变量的C语言代码,大都可以抽象成类似于下面这样的例子,请看:
int i = 5;
int j = 10;
int result = i+j;
到这里,有些小伙伴就有疑问了,使用临时中间变量会降低C语言程序的效率吗?或者说,将上面的 3 行代码写成一行:
int result = 5+10;
是否能够得到更高效率的C语言程序呢?鉴于使用临时中间变量常常能够提升代码的可读性,要是它同时真的会降低程序效率,我们就陷入矛盾了。
究竟有没有必要使用长表达式,代替临时中间变量呢?
其实,绝大多数现代C语言编译器已经足够聪明,优化上述代码轻而易举。我们还是以上述C语言代码为例,对其稍作调整,相关代码如下,请看:
#include <stdio.h>
void func()
{
int i = 5;
int j = 10;
int result = i + j;
printf( "%d\n", result ) ;
}
以 gcc 编译器为例,添加 -std=c99 指定 C99 标准,并指定 -O3 优化项,应该能够得到上述C语言代码对应的汇编代码:
movl $15, %esi
C语言编译器常用的一个策略“constant propagation(常数传播,下文讨论)”会将 i+j 在编译阶段计算完毕。
另外值得说明的是,我在上述C语言代码中添加了 printf() 函数读取 result,否则 fun() 函数将直接被优化成下面这样的汇编代码:
func:
rep ret
机器甚至都不会再去计算和保存 i+j,直接就返回了。C语言编译器做出这样的优化,显然可以避免无效代码消耗机器性能,因为 fun() 函数中并无实际的代码要用到 result,再去计算和处理它,显然就是做无用功。
当然了,若是考虑到硬件寄存器,即使没有代码使用到result,对其赋值也可能是有意义的。这是就需要一些其他操作,来避免编译器优化了。这一点,我之前的文章讨论过,感到陌生的读者可以翻翻看看。
可见,即使在C语言代码中使用了一些临时的中间变量,也不用太过担心它们会影响效率,因为这对于编译器来说,优化它们轻而易举。
编译器的“常数传播”和“常数折叠”策略
考虑到一些初学者不太熟悉编译器的策略,这里对前文设计的“常数传播”策略稍作解释。常数折叠(Constant folding)以及常数传播(constant propagation)都是C语言编译器对代码做优化使用的策略之一。
“常数折叠”是在编译期间简化常数的过程。常数在C语言程序中仅仅代表一个简单的数值,事实上,若某个变量从未被修改,完全可以将其也当作常数,请看下面这个例子:
i = 320 * 200 * 32;
多数现代C语言编译器不会真的产生两个乘法指令,然后再将结果存储起来,而是辨识是否之后没有代码修改变量 i,若如此,则会在编译阶段就将结果计算出来,之后直接使用计算结果。此即一次典型的“常数折叠”过程,这样做显然可以减小C语言代码尺寸,并且能够得到更高效的程序。
“常数传播”则是一个替代表达式中已知常数的过程,也是在编译时执行的,这里仍然以实例讨论,请看下面这段C语言代码:
int x = 14;
int y = 7 - x / 2;
return y * (28 / x + 2);
编译器执行“常数传播”策略,则 x 会被替换为常数 14,得到下面这样的C语言代码:
int x = 14;
int y = 7 - 14 / 2;
return y * (28 / 14 + 2);
继续这一传播过程,则最终得到下面这样的C语言代码:
int x = 14;
int y = 0;
return 0;
编译器通常还会执行一些消除无用代码的过程,以对整个C语言程序优化,因此在现代编译器看来:
int x = 14;
int y = 7 - x / 2;
return y * (28 / x + 2);
其实和简单的 return 0; 没什么两样。
小结
本文在最后介绍了现代C语言编译器常用的“常数折叠”和“常数传播”策略,可见,如今的编译器早已不再是只会逐行“死板”的处理代码了,因此,我们在编写C语言代码时,除非在某些特殊的情况下,否则更多的应该关心代码的可读性。那些复杂的“优化”工作,交给编译器去做就好了。
https://zh.wikipedia.org/wiki/%E5%B8%B8%E6%95%B8%E6%8A%98%E7%96%8A