CSAPP 5.1 优化编译器的能力和局限性

5.1 优化编译器的能力和局限性

现代编译器运用复杂精细的算法来确定一个程序中计算的值,以及他们是被如何使用的,然后利用一些机会来简化表达式,如:在几个不同的地方使用同一个计算,以及降低一个给定的计算必须被执行的次数。大多数编译器,包括 GCC,向用户提供了一些对它们所使用的优化的控制。就像在第3章中讨论过的,最简单的控制就是指定优先级别。例如,以命令行选项 “-Og” 调用 GCC 是让 GCC 使用一组基本的优化。以选项 “-O1” 或更高(如 “-O2” 或 “-O3” ) 调用 GCC 会让他使用更多的优化,进一步提高程序的性能,但是也可能增加程序的规模,也可能使标准的调试工具更难对程序进行调试。

编译器必须很小心地对程序进行安全的优化,即对于程序可能遇到的所有可能的情况,在C语言标准提供的保证之下,优化后得到的程序和为优化的版本有一样的行为。这一限制消除了一些造成不希望的运行时行为的隐患,但是也意味着程序员要花费更多精力写出编译器能够将之转换成有效及其代码的程序。下面这个简单的例子帮助理解编译器优化限制:

void twiddle1(long *xp, long *yp) {
    *xp += *yp;
    *xp += *yp;
}

void twiddle2(long *xp, long *yp) {
	*xp += 2* *yp;
}

twiddle1 和 twiddle2 两个函数“似乎”有相同的功能(把存储在指针yp指向的值两次加到指针xp所指的值上)。从另一个角度来看,函数 twiddle2 效率更高一些,它只要求三次内存引用(读*xp、读*yp、写*xp),而 twiddle1需要6次(重复twiddle2需要的内存引用操作两次)。因此,如果要编译器编译函数 twiddle1,我们认为基于 twiddle2 执行的计算能产生更有效的代码。.

不过,考虑到 xp 和 yp 指向同一地址(xp 和 yp相等)的情况,此时,函数 twiddle1 的结果使 xp 变为原来的 4 倍,函数 twiddle2 的结果使 xp 变为原来的 3 倍。编译器不知道 twiddle1 会在何种情况被调用,因此必须假设 xp 和 yp 可能会相等。因此,编译器不能产生 twiddle2 风格的代码作为 twiddle1 的优化版本。这种两个指针指向同一个内存地址的情况称为 memory aliasing(内存别名使用),这可能会造成一个主要的妨碍优化的因素,限制了可能的优化策略。


第二个妨碍优化的因素是 函数调用。考虑下面这个示例:

long f();

long func1() {
	return f()+f()+f()+f();
}

long func2() {
	return 4 * f();
}

最初看上去两个函数实现的都是相同的功能,但是 func2() 只调用 f() 1次,而 func1() 调用 f() 4次。以func1() 作为源代码时,会很想产生 func2() 风格的代码。不过考虑下面 有关 f() 的代码:

long counter = 0;

long f() {
	return counter++;
}

这个函数有个“副作用”,他修改了全局程序状态的一部分,改变该函数的调用次数会改变程序的功能。假设开始时去全局变量 counter 的值设为0,调用 func1() 会返回 0+1+2+3=6,而调用 func2()返回 4*0=0。大多数编译器会试图判断一个函数是否有副作用,如果有,编译器会假设最坏的情况并保持所有的函数调用不变。反之,则会进行一定程度的优化。


用内联优化函数调用

包含函数函数调用的代码可以用 inline substitution(内敛函数替换) 或者简称 inlining(内联) 的过程进行优化,此时,将函数调用替换为函数体。下面是 func1() 的内联版代码:

/* Result of inlining f in func1 */
long funclin() {
    long t = counter++;  /* +0 */
    t += counter++;      /* +1 */
    t += counter++;      /* +2 */
    t += counter++;      /* +3 */
    return t;
}

这样的转换即减少了函数调用的开销,也允许对展开的代码做进一步优化。例如,编译器可以统一funclin() 中对全局变量的更新,产生这个函数的一个优化版本:

/* Optimization of inlined func1 code */
long funclopt() {
	long t = 4 * counter + 6;
	counter += 4;
	return t;
}

对于这个特定的函数 f 的定义,上述代码重现了 func1 的行为。

最近版本的 GCC 会尝试进行这种形式的优化,可以用命令行选项-finline指示编译器,或者使用优化等级 “-O1” 或更高等级。但是目前 GCC 尝试在单个文件中定义的函数的内联,意味着它无法用于常见的情况(一组库函数在一个文件中被定义,在其他文件的函数中被调用)。

在某些情况下,最好能阻止编译器的内联优化。一种情况是 用符号调试器(如GDB)来评估代码,如果一个函数调用已经用内联替换优化过了,那么任何对这个调用进行追踪或设置断点的尝试都会失败。还有一种情况是用代码剖析的方式来评估程序性能,用内联替换消除的函数调用是不能被正确剖析的。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值