C++性能优化笔记-7-编译器中的优化-3-编译器、CPU优化的障碍

编译器优化的障碍

有几个因素会阻止编译器进行预期的优化。程序员知道这些障碍并知道如何避免它们。优化的某些重要的障碍讨论如下。

不能跨模块优化

编译器没有除正在编译模块以外其他模块中的函数信息。这阻止了跨函数调用的优化。如:

// Example 8.20

**module1.cpp**
int Func1(int x) {
     return x*x + 1;
}

**module2.cpp**
int Func2() {
     int a = Func1(2);
     ...
}

如果Func1Func2在相同的模块中,编译器能够进行函数内联与常量传播,并约简a为常量5。但在编译module2.cpp时,编译器没有Func1的必要信息。

解决这个问题最简单的方法是通过#include指示,将多个.cpp模块合并为一个。这确定在所有的编译器上都能工作。某些编译器有称为整体程序优化的特性,它将启用跨模块优化。

指针别名

当通过指针或引用访问变量时,编译器不能完全排除指向的变量与代码中其他某个变量相同的可能性。例子:
。。。

大多数编译器有用于假设没有指针别名的选项(/Oa)。克服指针别名障碍最简单的方法是打开这个选项。这要求仔细分析代码中所有指针与引用,确保在代码的同一部分没有多种方式访问变量或对象。如果编译器支持,通过使用关键字__restrict__restrict__,告诉编译器一个特定指针不会有别名也是可能的。

我们不能确定编译器会采纳没有指针别名的暗示。确保代码被优化的方式是显式进行。在例子8.21中,你可以计算*p+2,把它保存在循环外的一个临时变量中,如果你确定该指针不是数组中任何元素的别名。这个方法要求你可以预测优化的障碍的哪里。

动态内存分配

任何动态分配(使用newmalloc)的数组或对象必须通过指针访问。对程序员来说,指向不同动态分配对象的指针不会重叠或互为别名是显而易见的,但编译器通常不能看到。它也阻止编译器最优地对齐数据,或知道对象是对齐的。最好在需要它们的函数里声明对象及固定大小的数组。

纯函数

纯函数是没有副作用,返回值仅依赖其实参值的函数。这紧密遵循“函数”的数学概念。

以相同实参多次调用一个纯函数肯定产生相同的结果。编译器可以消除包含纯函数的公共子表达式,并且可以移出包含纯函数调用的循环不变代码。不幸的是,编译器不能知道一个函数是纯函数,如果它定义在另一个模块或函数库里。

因此,在涉及纯函数调用时,手动进行比如公共子表达式消除、常量传播及循环不变代码移动的优化,是必须的。

用于Linux的Gnu编译器、Clang编译器及Intel编译器有可应用于函数原型的一个属性,告诉编译器这是一个纯函数。例子:
。。。

其他某些编译器(Microsoft,Intel)知道标准库函数,像sqrtpowlog是纯函数,但不幸的是,没有办法告诉这些编译器一个自定义函数是纯函数。

虚函数与函数指针

编译器很少能确定地预测将调用虚函数的哪个版本,或者函数指针指向哪个函数。因此,它不能内联这个函数,或者跨函数调用优化。

代数化简

大多数编译器可以进行简单的代数约简,比如-(-a) = a,但它们不能进行更复杂的约简,代数约简是一个很难在编译器中实现的复杂过程。

由于数学纯粹性的原因,许多代数约简不被允许。在许多情形里,构造模糊的例子,其中约简会导致溢出或精度损失,特别在浮点表达式中。编译器不能排除一个特定约简在一个特定情形下无效的可能性,但程序员可以。因此,在许多情形里,显式进行代数约简是必须的。

整形表达式不那么容易受溢出以及精度损失的影响。因此,对编译器,在整形表达式上,比在浮点表达式上,进行更多约简是可能的。大多数涉及整数加法、减法及乘法的约简是被允许的,而许多涉及除法与关系操作符(如>)的约简,出于数学的原因,不被允许。例如,编译器不能将整形表达式-a > -b约简为a < b,因为溢出非常不确定的可能性。

浮点归纳变量

编译器不能创建浮点归纳变量,原因与它们不能在浮点表达式上进行代数约简的相同,因此,手动进行是必要的。一旦一个循环计数器函数可以根据之前值更高效计算时,这个原则是有用的。任何是循环计数器n阶多项式的表达式可以通过n个加法计算而不用乘法。下面的例子展示了用于二阶多项式的原则:
。。。

归纳变量的方法也可以向量化,如果考虑到从序列里r个位置之前的值计算每个值,其中r是向量中元素个数或者循环展开因子。在每个情形里,找出正确的公式要求一点数学。

具有非内联拷贝的内联函数

函数内联有从另一个模块调用此函数而产生的复杂性。如果内联函数可能被另一个模块调用,编译器必须制作内联函数的一个非内联拷贝。如果没有其他模块调用这个函数,这个非内联拷贝是死代码。这个代码片段使得缓冲效率下降。

围绕这个问题有各种方法。如果没有从其他模块援引一个函数,那么向这个函数声明添加关键字static。这告诉编译器这个函数不能被其他模块调用。static声明使得编译器评估内联这个函数是否最优更容易,并且它阻止编译器制作一个不使用的内联函数拷贝。static关键字也使得其他优化成为可能,因为编译器不需要遵守用于其他模块不可访问函数的任何特定的调用惯例。你可以对使用本地非成员函数添加static关键字。

不幸的是,这个方法对类成员函数不奏效,因为static关键字对成员函数有不同的含义。你可以通过在类定义中声明函数体,迫使内联成员函数。这将阻止编译器制作该函数的一个非内联拷贝,但它有缺点:该函数总是被内联,即使这样做不是最优。(即成员函数庞大且从许多不同地方调用)。

某些编译器有允许链接器删除未引用函数的选项(Windows:/Gy,Linux:-ffunction-sections)。建议打开这个选项。

CPU优化的障碍

现代CPU通过乱序执行指令,可以进行大量的优化。代码中的长依赖链阻止了CPU进行乱序执行。避免长依赖链,特别是长时延的循环依赖链。

欢迎交流
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值