8.2. 不同编译器的比较
我在7个不同品牌的C++编译器上进行了一系列实验,看它们是否能够进行各种优化。结果汇总在表8.1中。这个表展示了在我的测试例子中,不同的编译器是否成功应用了各种优化方法以及代数约简。
这个表可以给出哪些优化你可以预期一个特定的编译器会进行,哪些优化你必须手动进行的某些预示。
必须强调,在不同的测试例子上,编译器行为会不同。你不能预期编译器的行为总是符合这个表。
优化方法 |
Microsoft |
Borland |
Intel |
Gnu |
PathScale |
PGI |
Digital Mars |
Watcom |
Codeplay |
函数内联 |
x |
- |
x |
x |
x |
x |
- |
- |
x |
常量折叠 |
x |
x |
x |
x |
x |
x |
x |
x |
x |
常量传播 |
x |
- |
x |
x |
x |
x |
- |
- |
x |
指针消除 |
x |
x |
x |
x |
x |
x |
x |
x |
x |
|
|
|
|
|
|
|
|
|
|
公共子表达式消除,整形 |
x |
(x) |
x |
x |
x |
x |
x |
x |
x |
公共子表达式消除,浮点 |
x |
- |
x |
x |
x |
x |
- |
x |
x |
寄存器变量,整形 |
x |
x |
x |
x |
x |
x |
x |
x |
x |
寄存器变量,浮点 |
x |
- |
x |
x |
x |
x |
- |
x |
x |
生命期分析 |
x |
x |
x |
x |
x |
x |
x |
x |
x |
合并相同的分支 |
x |
- |
- |
x |
- |
- |
- |
x |
- |
消除跳转 |
x |
x |
x |
x |
x |
x |
- |
x |
x |
消除分支 |
x |
- |
x |
x |
x |
x |
- |
- |
- |
删除总是true/false的分支 |
x |
- |
x |
x |
x |
x |
x |
x |
x |
循环展开 |
x |
- |
x |
x |
x |
x |
- |
- |
x |
循环不变代码移动 |
x |
- |
x |
x |
x |
x |
x |
x |
x |
数组元素归纳变量 |
x |
x |
x |
x |
x |
x |
x |
x |
x |
其他整型表达式归纳变量 |
x |
- |
x |
x |
x |
- |
x |
x |
x |
浮点表达式归纳变量 |
- |
- |
- |
- |
- |
- |
- |
- |
- |
自动向量化 |
- |
- |
x |
x |
x |
x |
- |
- |
x |
去虚拟化 |
- |
- |
- |
x |
- |
- |
- |
- |
- |
分析指引(profile-guided)优化 |
x |
- |
x |
x |
x |
x |
- |
- |
- |
整体程序优化 |
x |
- |
x |
x |
x |
- |
- |
- |
- |
|
|
|
|
|
|
|
|
|
|
整形代数约简: |
|
|
|
|
|
|
|
|
|
a+b = b+a |
x |
(x) |
x |
x |
x |
x |
- |
x |
x |
a*b = b*a |
x |
(x) |
x |
x |
x |
x |
- |
x |
x |
(a+b)+c = a+(b+c) |
x |
- |
x |
x |
- |
- |
x |
x |
- |
a+b+c = c+b+a |
x |
- |
- |
x |
- |
- |
- |
- |
- |
a+b+c+d = (a+b)+(c+d) |
- |
- |
x |
x |
- |
- |
- |
- |
- |
a*b+a*c = a*(b+c) |
x |
- |
x |
x |
x |
- |
- |
- |
x |
a*x*x*x + b*x*x + c*x + d = ((a*x+b)*x+c)*x+d |
x |
- |
x |
x |
x |
- |
- |
- |
x |
x*x*x*x*x*x*x*x = ((x2) 2) 2 |
- |
- |
x |
- |
- |
- |
- |
- |
- |
a+a+a+a = a*4 |
x |
- |
x |
x |
- |
- |
- |
- |
x |
-(-a) = a |
x |
- |
x |
x |
x |
x |
x |
x |
- |
a-(-b) = a+b |
x |
- |
x |
x |
x |
x |
- |
x |
- |
a-a = 0 |
x |
- |
x |
x |
x |
x |
x |
x |
x |
a+0 = a |
x |
x |
x |
x |
x |
x |
x |
x |
x |
a*0 = 0 |
x |
x |
x |
x |
x |
x |
x |
- |
x |
a*1 = a |
x |
x |
x |
x |
x |
x |
x |
x |
x |
(-a)*(-b) = a*b |
x |
- |
x |
x |
x |
- |
- |
- |
- |
a/a = 1 |
- |
- |
- |
- |
x |
- |
- |
- |
x |
a/1 = a |
x |
x |
x |
x |
x |
x |
x |
x |
x |
0/a = 0 |
- |
- |
- |
x |
- |
- |
- |
x |
x |
(-a == -b) = (a == b) |
- |
- |
- |
x |
x |
- |
- |
- |
- |
(a+c == b+c) = (a == b) |
- |
- |
- |
- |
x |
- |
- |
- |
- |
!(a < b) = (a >= b) |
x |
x |
x |
x |
x |
x |
x |
x |
x |
(a |
- |
- |
- |
- |
- |
- |
- |
- |
- |
乘以常量 = 偏移与加法 |
x |
x |
x |
x |
- |
x |
x |
x |
- |
除以常量 = 乘法与偏移 |
x |
- |
x |
x |
x |
(-) |
x |
- |
- |
|
|
|
|
|
|
|
|
|
|
浮点代数约简: |
|
|
|
|
|
|
|
|
|
a+b = b+a |
x |
- |
x |
x |
x |
x |
- |
- |
x |
a*b = b*a |
x |
- |
x |
x |
x |
x |
- |
- |
x |
a+b+c = a+(b+c) |
x |
- |
x |
x |
- |
- |
- |
- |
- |
(a+b)+c = a+(b+c) |
- |
- |
x |
x |
- |
- |
- |
- |
- |
a*b*c = a*(b*c) |
x |
- |
- |
x |
- |
- |
- |
- |
- |
a+b+c+d = (a+b)+(c+d) |
- |
- |
- |
x |
- |
- |
- |
- |
- |
a*b+a*c = a*(b+c) |
x |
- |
- |
- |
x |
- |
- |
- |
x |
a*x*x*x + b*x*x + c*x + d = ((a*x+b)*x+c)*x+d |
x |
- |
x |
x |
x |
- |
- |
- |
- |
x*x*x*x*x*x*x*x = ((x2) 2) 2 |
- |
- |
- |
x |
- |
- |
- |
- |
- |
a+a+a+a = a*4 |
x |
- |
- |
x |
x |
- |
- |
- |
- |
-(-a) = a |
- |
- |
x |
x |
x |
x |
x |
x |
- |
a-(-b) = a+b |
- |
- |
- |
x |
x |
x |
- |
x |
- |
a+0 = a |
x |
- |
x |
x |
x |
x |
x |
x |
- |
a*0 = 0 |
- |
- |
x |
x |
x |
x |
- |
x |
x |
a*1 = a |
x |
- |
x |
x |
x |
x |
x |
- |
x |
(-a)*(-b) = a*b |
- |
- |
- |
x |
x |
x |
- |
- |
- |
a/a = 1 |
- |
- |
- |
- |
- |
- |
- |
- |
x |
a/1 = a |
x |
- |
x |
x |
x |
- |
x |
- |
- |
0/a = 0 |
- |
- |
- |
x |
x |
- |
- |
x |
x |
(-a == -b) = (a == b) |
- |
- |
- |
x |
x |
- |
- |
- |
- |
(-a > -b) = (a < b) |
- |
- |
- |
x |
x |
- |
- |
- |
x |
除以常量 = 乘以倒数 |
x |
x |
- |
x |
x |
- |
- |
x |
- |
|
|
|
|
|
|
|
|
|
|
布尔代数约简: |
|
|
|
|
|
|
|
|
|
!(!a) = a |
x |
- |
x |
x |
x |
x |
x |
x |
x |
(a&&b) || (a&&c) = a&&(b||c) |
x |
- |
x |
x |
x |
- |
- |
- |
- |
!a && !b = !(a || b) |
x |
x |
x |
x |
x |
x |
x |
x |
x |
a && !a = false, a || !a = true |
x |
- |
x |
x |
x |
x |
- |
- |
- |
a && true = a, a || false = a |
x |
x |
x |
x |
x |
x |
x |
x |
- |
a && false = false, a || true = true |
x |
- |
x |
x |
x |
x |
x |
x |
- |
a && a = a |
x |
- |
x |
x |
x |
x |
- |
- |
- |
(a&&b) || (a&&!b) = a |
x |
- |
- |
x |
x |
- |
- |
- |
- |
(a&&b) || (!a&&c) = a ? b : c |
x |
- |
x |
x |
- |
- |
- |
- |
- |
(a&&b) || (!a&&c) || (b&&c) = a ? b : c |
x |
- |
- |
x |
- |
- |
- |
- |
- |
(a&&b) || (a&&b&&c) = a&&b |
x |
- |
- |
x |
x |
- |
- |
- |
- |
(a&&b) || (a&&c) || (a&&b&&c) = a&&(b||c) |
x |
- |
- |
x |
x |
- |
- |
- |
- |
(a&&!b) || (!a&&b) = a XOR b |
- |
- |
- |
- |
- |
- |
- |
- |
- |
|
|
|
|
|
|
|
|
|
|
比特向量代数约简: |
|
|
|
|
|
|
|
|
|
~(~a) = a |
x |
- |
x |
x |
x |
x |
x |
- |
- |
(a&b)|(a&c) = a&(b|c) |
x |
- |
x |
x |
x |
x |
- |
- |
x |
(a|b)&(a|c) = a|(b&c) |
x |
- |
x |
x |
x |
x |
- |
- |
x |
~a & ~b = ~(a | b) |
- |
- |
x |
x |
x |
x |
- |
- |
- |
a & a = a |
x |
- |
- |
x |
x |
x |
- |
- |
x |
a & ~a = 0 |
- |
- |
x |
x |
x |
x |
- |
- |
- |
a & -1 = a, a | 0 = a |
x |
- |
x |
x |
x |
x |
x |
x |
x |
a & 0 = 0, a | -1 = -1 |
x |
- |
x |
x |
x |
x |
x |
x |
x |
(a&b) | (~a&c) | (b&c) = (a&b) | (~a&c) |
- |
- |
- |
- |
- |
- |
- |
- |
- |
a&b&c&d = (a&b)&(c&d) |
- |
- |
- |
x |
- |
- |
- |
- |
- |
a ^ 0 = a |
x |
x |
x |
x |
x |
- |
x |
x |
x |
a ^ -1 = ~a |
x |
- |
x |
x |
x |
- |
x |
x |
- |
a ^ a = 0 |
x |
- |
x |
x |
x |
x |
- |
x |
x |
a ^ ~a = -1 |
- |
- |
- |
x |
x |
x |
- |
- |
- |
(a&~b) | (~a&b) = a ^ b |
- |
- |
- |
- |
- |
- |
- |
- |
- |
~a ^ ~b = a ^ b |
- |
- |
- |
x |
x |
- |
- |
- |
- |
a< |
x |
- |
x |
x |
x |
- |
- |
x |
x |
|
|
|
|
|
|
|
|
|
|
整形XMM(向量)约简: |
|
|
|
|
|
|
|
|
|
公共子表达式消除 |
x |
n.a. |
x |
x |
x |
- |
n.a. |
n.a. |
x |
常量折叠 |
- |
n.a. |
- |
x |
- |
- |
n.a. |
n.a. |
- |
a+b = b+a, a*b = b*a |
- |
n.a. |
- |
x |
- |
- |
n.a. |
n.a. |
x |
(a+b)+c = a+(b+c) |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
a*b+a*c = a*(b+c) |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
x*x*x*x*x*x*x*x = ((x2) 2) 2 |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
a+a+a+a = a*4 |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
-(-a) = a |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
a-a = 0 |
- |
n.a. |
x |
- |
- |
- |
n.a. |
n.a. |
- |
a+0 = a |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
a*0 = 0 |
- |
n.a. |
- |
x |
- |
- |
n.a. |
n.a. |
- |
a*1 = a |
- |
n.a. |
- |
x |
- |
- |
n.a. |
n.a. |
- |
(-a)*(-b) = a*b |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
!(a < b) = (a >= b) |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
|
|
|
|
|
|
|
|
|
|
浮点XMM(向量): |
|
|
|
|
|
|
|
|
|
a+b = b+a, a*b = b*a |
x |
n.a. |
- |
x |
- |
- |
n.a. |
n.a. |
x |
a+b+c = a+(b+c) |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
a*b+a*c = a*(b+c) |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
-(-a) = a |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
a-a = 0 |
- |
n.a. |
- |
x |
- |
- |
n.a. |
n.a. |
- |
a+0 = a |
- |
n.a. |
x |
- |
- |
- |
n.a. |
n.a. |
- |
a*0 = 0 |
- |
n.a. |
x |
- |
- |
- |
n.a. |
n.a. |
- |
a*1 = a |
- |
n.a. |
- |
x |
- |
- |
n.a. |
n.a. |
- |
a/1 = a |
- |
n.a. |
- |
x |
- |
- |
n.a. |
n.a. |
- |
0/a = 0 |
- |
n.a. |
x |
x |
- |
- |
n.a. |
n.a. |
- |
除以常量 = 乘以倒数 |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
|
|
|
|
|
|
|
|
|
|
布尔XMM(向量)约简: |
|
|
|
|
|
|
|
|
|
~(~a) = a |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
(a&b)|(a&c) = a&(b|c) |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
a & a = a, a | a = a |
- |
n.a. |
x |
x |
- |
- |
n.a. |
n.a. |
- |
a & ~a = 0 |
- |
n.a. |
- |
x |
- |
- |
n.a. |
n.a. |
- |
a & -1 = a, a | 0 = a |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
a & 0 = 0 |
- |
n.a. |
- |
x |
- |
- |
n.a. |
n.a. |
- |
a | -1 = -1 |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
a ^ a = 0 |
- |
n.a. |
x |
x |
- |
- |
n.a. |
n.a. |
- |
andnot(a,a) = 0 |
- |
n.a. |
- |
x |
- |
- |
n.a. |
n.a. |
- |
a< |
- |
n.a. |
- |
- |
- |
- |
n.a. |
n.a. |
- |
表8.1. 不同C++编译器里优化的比较 测试在打开所有相关优化选项时进行,包括放松浮点精度。测试了以下编译器版本: Microsoft C++ Compiler v. 14.00 for 80x86 / x64 (Visual Studio 2005). Borland C++ 5.82 (Embarcadero/CodeGear/Borland C++ Builder 5, 2009). Intel C++ Compiler v. 11.1 for IA-32/Intel64, 2009. Gnu C++ v. 4.1.0, 2006 (Red Hat). PathScale C++ v. 3.1, 2007. PGI C++ v. 7.1-4, 2008. Digital Mars Compiler v. 8.42n, 2004. Open Watcom C/C++ v. 1.4, 2005. Codeplay VectorC v. 2.1.7, 2004. 对Microsoft、Intel、Gnu与PathScale编译器,对32位与64位代码没有观察到优化能力间的差异 |
8.3. 编译器优化的障碍
有几个因素会阻止编译器进行我们希望它做的优化。程序员知道这些障碍并知道如何避免它们是重要的。优化的某些重要的障碍讨论如下。
编译器没有除正在编译模块以外其他模块中的函数信息。这阻止了跨函数调用的优化。如:
// Example 8.20
module1.cpp
int Func1(int x) {
return x*x + 1;
}
module2.cpp
int Func2() {
int a = Func1(2);
...
}
如果Func1与Func2在相同的模块中,编译器能够进行函数内联与常量传播,并约简为常量5。但在编译module2.cpp时,编译器没有Func1的必要信息。
解决这个问题最简单的方法是通过#include指示,将多个.cpp模块合并为一个。这确定在所有的编译器上都能工作。某些编译器有称为整体程序优化的特性,它将启用跨模块优化(参考第82页)。
当通过指针或引用访问变量时,编译器不能完全排除指向的变量与代码中其他某个变量相同的可能性。例子:
// Example 8.21
void Func1 (int a[], int * p) {
int i;
for (i = 0; i < 100; i++) {
a[i] = *p + 2;
}
}
void Func2() {
int list[100];
Func1(list, &list[8]);
}
这里,重新载入*p并计算*p+2一百次是必须的,因为p指向的值与a[]中在循环期间改变的其中一个元素相同。不允许假设*p+2是可以移出循环的循环不变代码。例子8.21确实是一个非常做作的例子,但要点是编译器不能排除理论上这样做作例子存在的可能性。因此,编译器被阻止假设*p+2是可以移出循环的循环不变表达式。
大多数编译器有用于假设没有指针别名的选项(/Oa)。克服可能指针别名障碍最简单的方法是关闭这个选项。这要求你仔细分析代码中所有指针与引用,确保在代码的同一部分没有多种方式访问变量或对象。如果编译器支持,通过使用关键字__restrict或__restrict__,告诉编译器一个特定指针不会有别名也是可能的。
我们不能确定编译器会采纳没有指针别名的暗示。确保代码被优化的仅有方式是显式进行。在例子8.21中,你可以计算*p+2,把它保存在循环外的一个临时变量中,如果你确定该指针不是数组中任何元素的别名。这个方法要求你可以预测优化的障碍的哪里。
任何动态分配(使用new或malloc)的数组或对象必须通过指针访问。对程序员来说,指向不同动态分配对象的指针不会重叠或互为别名是显而易见的,但编译器通常不能看到这。它也阻止编译器最优地对齐数据,或知道对象是对齐的。最好在需要它们的函数里声明对象及固定大小的数组。
纯函数是没有副作用,返回值仅依赖其实参值的函数。这紧密遵循“函数”的数学概念。
以相同实参多次调用一个纯函数肯定产生相同的结果。编译器可以消除包含纯函数的公共子表达式,并且可以移出包含纯函数调用的循环不变代码。不幸的是,编译器不能知道一个函数是纯函数,如果它定义在另一个模块或函数库里。
因此,在涉及纯函数调用时,手动进行比如公共子表达式消除、常量传播及循环不变代码移动的优化,是必须的。
用于Linux的Gnu编译器及Intel编译器有可应用于函数原型的一个属性,告诉编译器这是一个纯函数。例子:
// Example 8.22
#ifdef __GNUC__
#define pure_function __attribute__((const))
#else
#define pure_function
#endif
double Func1(double) pure_function ;
double Func2(double x) {
return Func1(x) * Func1(x) + 1.;
}
这里,Gnu编译器将仅对Func1进行一次调用,而其他编译器将进行两次。
其他某些编译器(Microsoft,Intel)知道标准库函数,像sqrt、pow与log是纯函数,但不幸的是,没有办法告诉这些编译器一个用户定义函数是纯函数。
编译器很少能确定地预测将调用虚函数的哪个版本,或者函数指针指向哪个函数。因此,它不能内联这个函数,或者跨函数调用优化。
大多数编译器可以进行简单的代数约简,比如-(-a) = a,但它们不能进行更复杂的约简,代数约简是一个很难在编译器中实现的复杂过程。
由于数学纯粹性的原因,许多代数约简不被允许。在许多情形里,构造模糊的例子,其中约简会导致溢出或精度损失,特别在浮点表达式中,是可能的(参考第59页)。编译器不能排除一个特定约简在一个特定情形下无效的可能性,但程序员可以。因此,在许多情形里,显式进行代数约简是必须的。
整形表达式不那么容易受溢出以及精度损失的影响,原因在第70页解释。因此,对编译器,在整形表达式上,比在浮点表达式上,进行更多约简是可能的。大多数涉及整数加法、减法及乘法的约简是被允许的,而许多涉及除法与关系操作符(如>)的约简,出于数学纯粹性的原因,不被允许。例如,编译器不能将整形表达式-a > -b约简为a < b,因为溢出非常不确定的可能性。
表8.1(第62页)展示了编译器能够进行哪些约简,至少在某些情形里,以及不能执行哪些约简。所有编译器不能进行的约简必须由程序员手动进行。
编译器不能制作浮点归纳变量,与它们不能在浮点表达式上进行代数约简的原因相同,因此,手动进行是必要的。一旦一个循环计数器函数可以根据之前值更高效计算时,这个原则是有用的。任何是循环计数器n阶多项式的表达式可以通过n个加法计算,没有乘法。下面的例子展示了用于二阶多项式的原则:
// Example 8.23a. Loop to make table of polynomial
const double A = 1.1, B = 2.2, C = 3.3; // Polynomial coefficients
double Table[100]; // Table
int x; // Loop counter
for (x = 0; x < 100; x++) {
Table[x] = A*x*x + B*x + C; // Calculate polynomial
}
这个多项式的计算可以通过使用两个归纳变量的两次加法来完成:
// Example 8.23b. Calculate polynomial with induction variables
const double A = 1.1, B = 2.2, C = 3.3; // Polynomial coefficients
double Table[100]; // Table
int x; // Loop counter
const double A2 = A + A; // = 2*A
double Y = C; // = A*x*x + B*x + C
double Z = A + B; // = Delta Y
for (x = 0; x < 100; x++) {
Table[x] = Y; // Store result
Y += Z; // Update induction variable Y
Z += A2; // Update induction variable Z
}
例子8.23b中的循环有两个循环携带依赖链,即是两个归纳变量Y与Z。每条依赖链有与浮点加法相同的时延。小到足以支持这个方法。更长的循环依赖链将使得归纳变量不再有利,除非该值从一个两个或更多迭代以前的值计算。
归纳变量的方法也可以向量化,如果考虑到从序列里r个位置之前的值计算每个值,其中r是向量中元素个数或者循环展开因子。在每个情形里,找出正确的公式要求一点数学。
函数内联有相同的函数可以从另一个模块调用的复杂性。因为函数也可以从另一个模块调用的可能性,编译器必须制作内联函数的一个非内联拷贝。如果没有其他模块调用这个函数,这个非内联拷贝是死代码。这个代码片段使得缓冲效率下降。
围绕这个问题有各种方法。如果没有从其他模块援引一个函数,那么向这个函数声明添加关键字static。这告诉编译器这个函数不能被其他模块调用。Static声明使得编译器评估内联这个函数是否最优更容易,并且它阻止编译器制作一个不使用的内联函数拷贝。Static关键字也使得其他优化成为可能,因为编译器不需要遵守用于其他模块不可访问函数的任何特定的调用惯例。你可以对使用本地非成员函数添加static关键字。
不幸的是,这个方法对类成员函数不奏效,因为static关键字对成员函数有不同的含义。你可以通过在类定义中声明函数体,迫使内联成员函数。这将阻止编译器制作该函数的一个非内联拷贝,但它有该函数总是被内联,即使这样做不是最优的缺点(即,如果成员函数大且从许多不同地方调用)。
某些编译器有允许链接器删除未定义函数的选项(Windows:/Gy,Linux:-ffunction-sections)。建议打开这个选项。
8.4. CPU优化的障碍
现代CPU通过乱序执行指令,可以进行大量的优化。代码中的长依赖链阻止了CPU进行乱序执行,如第14页所述。避免长依赖链,特别是长时延的循环携带依赖链。