优化程序性能
编写运行的快的程序有三个因素:
- 选择合适的算法和数据结构;
- 理解编译器的能力,必须编写出编译器可以有效优化以转化为高效可执行代码的源代码;
在C语言中,指针运算和强制类型转换使得编译器很难对它进行优化; - 对于运算量特别大的程序,可能还需要进行任务分解。这些子任务可以利用多核或多处理器并行计算;
在这一过程中可能还需要对程序的可读性和运行速度进行权衡。
编译技术被分为**“与机器无关”和“与机器相关”**两类。“与机器无关”,使用这些技术时可以不考虑将执行代码的计算机的特性;而“与机器有关”,指这些技术依赖于许多机器的低级细节。
阻碍优化的因素
- 程序行为中那些严重依赖执行环境的方面
例如两条语句的执行有依赖关系
程序优化的步骤
- 消除不必要的工作,让代码尽可能有效地执行所期望的任务;
例如不必要的函数调用,内存引用等。(这很好理解) - 利用处理器提供的指令并行能力,同时执行多条指令;
这就要求要了解处理器的运作。 - 研究程序的汇编代码表示是理解编译器以及产生的代码会如何运行的最有效手段之一。
编译器的局限性
编译器必须很小心的对程序只使用安全的优化。
即保证优化前后的程序具有相同行为。(但是很明显,编译器没有那么智能,有些情况无法分辨处理,所以我们应该主动写出让编译器好优化的代码)
GCC优化指令
- Og:默认配置,不优化。
- O1:编译器试图优化代码大小和执行时间,它主要对代码的分支,常量以及表达式等进行优化,但不执行任何会占用大量编译时间的优化。
- O2:GCC执行几乎所有不包含时间和空间权衡的优化(比如,尝试更多的寄存器级的优化以及指令级的优化)。与-O相比,此选项增加了编译时间,但提高了代码的效率。
- O3:比-O2更优化,对于-O3编译选项,在-O2的基础上,打开了更多的优化项(比如,使用伪寄存器网络,普通函数的内联,以及针对循环的更多优化)。不过可能导致编译出来的二级制程序不能debug。
- Os:主要是对代码大小的优化,我们基本不用做更多的关心。 通常各种优化都会打乱程序的结构,让调试工作变得无从着手。并且会打乱执行顺序,依赖内存操作顺序的程序需要做相关处理才能确保程序的正确性。
内存别名
两种指针可能指向同一个内存位置的情况称为内存别名引用,在只执行安全的优化中,编译器必须假设不同的指针可能会指向内存的同一个位置。这样就限制了编译器的优化,如下面例子:
//原来的代码
void twiddle1(long *xp,long *yp)
{
*xp +=*yp;
*xp +=*yp;
}
//优化后的代码
void twiddle1(long *xp,long *yp)
{
*xp += 2*(*yp);
}
如果yp和xp指向同一个内存,原来的代码得到4倍结果,而优化后的代码却是3倍,故编译器这样优化会出问题
函数调用产生的问题:
long counter = 0;
long f()
{
return counter++;
}
//原始代码
long func1()
{
return f()+f()+f()+f();
}
//优化后的代码
long func2()
{
return 4*f();
}
虽然优化后只调用了一次f(),但是优化后得到的结果并不一样
解决办法:我们可以将f()声明为inline内敛函数,即调用的时候直接内部展开,这样既可以减少了函数调用的开销,也允许对展开的代码做进一步优化,适合一些经常被调用的少量代码
GCC只会尝试在单个文件中定义的函数内联。
表示程序性能
程序性能度量标准:每元素的周期数(Cycles Per Element,CPE)。
处理器活动的顺序是由时钟控制的,时钟提供了某个频率的规律信号,通常用千兆赫兹(GHz),即十亿周期每秒来表示。
例如,当表明一个系统有“4GHz”处理器,这表示处理器时钟运行频率为每秒(4 \times {10^9})个周期。
每个时钟周期的时间是时钟频率的倒数。通常是以纳秒( nanosecond,1纳秒等于({10^{ - 8}})秒)或皮秒( picosecond,1皮秒等于({10^{ - 12}})秒)为单位的。
例如,一个4GHz的时钟其周期为0.25纳秒,或者250皮秒。
从程序员的角度来看,用时钟周期来表示度量标准要比用纳秒或皮秒来表示有帮助得多。用时钟周期来表示,度量值表示的是执行了多少条指令,而不是时钟运行得有多快。
程序示例
打消循环的低效率(Code Motion)
举个例子如下所示:
void lower1(char *s)
{
size_t i;
for (i = 0; i < strlen(s); i++)
if (s[i] >= 'A' && s[i] <= 'Z')
s[i] -= ('A' - 'a');
}
程序看起来没什么问题,一个很平时的大小写转换的代码,然而为什么随着字符串输出长度的变长,代码的执行工夫会呈指数式增长呢?咱们把程序转换成GOTO模式看下。
参考资料
《深入理解计算机系统》 第三版