计算机系统基础之优化程序性能


前言

写程序最主要的目标就是使它在所有可能的情况下都正确工作。一个运行得很快但是给出错误结果的程序没有任何用处。程序员必须写出清晰简洁的代码,这样做不仅是为了自己能够看懂代码,也是为了在检查代码和今后需要修改代码时,其他人能够读懂和理解代码。
另一方面,在很多情况下,让程序运行得快也是一个重要的考虑因素。如果一个程序要实时地处理视频帧或者网络包,一个运行得很慢的程序就不能提供所需的功能。当一个计算任务的计算量非常大,需要执行数日或者数周,那么哪怕只是让它运行得快20%也会产生重大的影响。
编写高效程序需要做到以下几点:第一,我们必须选择一组适当的算法和数据结构。第二,我们必须编写出编译器能够有效优化以转换成高效可执行代码的源代码。对于这第二点,理解优化编译器的能力和局限性是很重要的。编写程序方式中看上去只是一点小小的变动,都会引起编译器优化方式很大的变化。有些编程语言比其他语言容易优化。C语言的有些特性,例如执行指针运算和强制类型转换的能力,使得编译器很难对它进行优化。程序员经常能够以一种使编译器更容易产生高效代码的方式来编写他们的程序。第三项技术针对处理运算量特别大的计算,将一个任务分成多个部分,这些部分可以在多核和多处理器的某种组合上并行地计算。


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

以命令行选项“-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;
}

乍一看,这两个过程似乎有相同的行为。它们都是将存储在由指针 yp指示的位置处的值两次加到指针xp指示的位置处的值。另一方面,函数twiddle2效率更高一些。它只要求3次内存引用,而twiddle1需要6次(2次读xp,2次读yp,2次写*xp)。因此,如果要编译器编译过程twiddle1,我们会认为基于twiddle2执行的计算能产生更有效的代码。
不过,考虑xp等于yp的情况。此时,函数twiddle1会执行如下操作

*xp += *xp;
*xp += *xp;

结果就是*xp的值变为原来的4倍

而twiddle2会执行如下操作

*xp += 2* *xp;

结果却是*xp的值变为原来的3倍

因此不能产生twiddle2风格的代码作为twiddle1的优化版本。
两个指针可能存在指向同一个内存位置的情况成为内存别名使用,在执行安全的优化时,编译器会假设不同的指针指向内存的同一个位置。再看如下的例子

x = 1000;
y = 3000;
*q = y;
*p = x;
t1 = *q;

t1的值依赖于p和q是否指向同一位置,如果p,q指向内存中的同一位置t1为1000,否则的话t1为3000。如果编译器不能确定两个指针是否指向同一个位置,就会限制可能的优化策略。

二、消除循环的低效率

combine1与combine2

在几个计算向量元素的乘积时

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
	}
}

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
	}
}

通过修改的版本可以得到如下图片所示的时间
在这里插入图片描述
这个优化是一类常见的优化的例子,成为代码移动。这类优化包括识别要执行多次但是计算结果不会改变的计算。因而可以将计算移动到代码前面不会被多次求值的部分。在本例中,将对vec_length的调用从循环内部移动到了循环外部。

三、消除不必要的内存应用

combine3与combine4

combine3的代码将合并运算计算的值积累在指针指定的地方,为了消除这种不必要的内存读写,引入一个临时变量acc,它在循环中用来积累计算出来的值。

void combine3(vec_ptr v, data_t *dest)
{
	long i;
	long length = vec_lenght(v);
	data_t *data = get_vec_start(v);

	*dest = IDENT;
	for(i = 0 ; i < lenght ; i ++ ) {
		*dest = *dest OP data[i];
	}
}

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 < lenght ; i ++) {
		acc = acc OP data[i];
	}
}

我们看到了程序的性能有了显著的提高,如下表所示
在这里插入图片描述
显然combine4的效率得到了更高的提升。

不幸的是,编译器不能判断函数会在什么情况下被调用,以及程序员的本意可能是什么,取而代之,在编译combine3时,保守的方法是不断的读和写内存,即使这样的效率不太高。


总结

到目前为止,我们运用的优化都不依赖于目标机器的任何特性。这些优化只是简单地降低了过程调用的开销,以及消除了一些重大的“妨碍优化的因素”,这些因素会给优化编译器造成困难。随着试图进一步提高性能,必须考虑利用处理器微体系结构的优化,也就是处理器用来执行指令的底层系统设计。要想充分提高性能,需要仔细分析程序,同时代码的生成也要针对目标处理器进行调整。尽管如此,我们还是能够运用一些基本的优化,在很大一类处理器上产生整体的性能提高。我们在这里公布的详细性能结果,对其他机器不一定有同样的效果,但是操作和优化的通用原则对各种各样的机器都适用。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值