5.1 优化编译器的能力和局限性
gcc 优化等级:
-O0
:这是默认的优化等级,基本上不进行任何优化,以便于调试。源代码会被尽可能地转化为目标代码,而不会对程序的执行速度进行优化。-O1
:这是一个适度的优化等级,它会在编译时执行一些简单的优化,如常量折叠、死代码删除、循环展开等,但不会影响编译时间太多。这个等级的优化通常不会显著改变程序的行为。-O2
:这是最常见的优化等级,它会执行更多的优化,包括函数内联、循环优化、寄存器分配等,可能会稍微增加编译时间,但是通常可以产生更快的可执行文件。-O3
:这是最高级别的优化,它包含了-O2 中的所有优化,并且会执行更激进的优化策略,如更多的内联、循环展开等。这可能会显著增加编译时间,但同时也可能产生执行效率最高的代码。-Os
:这个优化等级专注于减少输出文件的大小,同时仍然保持一定的运行时性能。它会使用一些特定的技巧来减小代码和数据的大小,适合用于嵌入式系统或资源有限的环境。-Og
:这个优化等级主要用于调试,它会生成更多的调试信息,同时尽量保持源代码和目标代码的一致性,以便于调试。-Ofast
:这是为了追求极致性能的优化等级,它会启用所有可用的优化,并且可能会违反 C/C++语言标准的一些规则,比如浮点运算的精度可能会有所损失。
两种指针可能指向同一个内存位置的情况称为内存别名使用。
比如
void twiddle1(long *xp, long *yp)
{
*xp += *yp;
*xp += *yp;
}
void twiddle2(long *xp, long *yp)
{
*xp = 2* *yp;
}
在xp
和yp
指针不指向同一地址时,twiddle1
可以被优化成twiddle2
,但如果xp == yp
,那结果就不一样了,twiddle1
该地址的值会变成 4 倍,而twiddle2
只是两倍。所以编译器对这种情况不能进行优化。
提供一个 combine1 函数,看看我们能如何来优化它。
// for add
#define IDENT 0
#define OP +
// for multiply
#define INENT 1
#define OP *
void combine1(vec_ptr v, data_t *dest)
{
long i;
*dest = IDENT;
for (i = 0; i < vec_length(v); i++) {
data_t val;
get_vec_element(v, i, &val);
*dest = *dest OP val;
}
}
int get_vec_element(vec_ptr v, long index, data_t *dest)
{
if (index < 0 || index >= v->Len)
return 0;
*dest = v->data[index];
return 1;
}
long vec_length(vec_ptr v)
{
return v->len;
}
5.4 消除循环的低效率
识别要执行多次但是计算结果不会改变的操作,放到循环外面。
combine1 中的vec_length()
可以放到循环外面。
void combine2(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
*dest = IDENT;
for (i = 0; i < length; i++) {
data_t val;
get_vec_element(v, i, &val);
*dest = *dest OP val;
}
}
5.5 减少过程调用
过程调用会带来开销。get_vec_element
不用每次循环都调用。
void combine3(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = get_vec_start(v);
*dest = IDENT;
for (i = 0; i < length; i++) {
*dest = *dest OP data[i];
}
}
5.6 消除不必要的内存引用
之前每次循环都会引用内存*dest
,可以提供一个局部变量来存储。
void combine4(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = get_vec_start(v);
data_t acc = IDENT;
for (i = 0; i < length; i++) {
acc = acc OP data[i];
}
*dest = acc;
}
5.7 理解现代处理器
// TODO:
5.8 循环展开
通过增加每次迭代计算的元素的数量,减少循环的迭代次数。
通过循环展开,
- 可以减少不直接有助于程序结果的操作的数量,例如循环索引计算和条件分支。
- 可以进一步变化代码,减少整个计算中关键路径上的操作数量。
void combine5(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
long limit = length-1;
data_t *data = get_vec_start(v);
data_t acc = IDENT;
// combine two elements at a time
for (i = 0; i < limit; i+=2) {
acc = (acc OP data[i]) OP data[i+1];
}
// finish any remaining elements
for (; i < length; i++) {
acc = acc OP data[i];
}
*dest = acc;
}
GCC 优化等级达到 3 或更高时,就会自动执行循环展开。
5.9 提高并行性
// TODO: