从系统方面优化程序性能几个基本要点

本文介绍从系统方面优化程序性能的方法。首先阐述优化编译器的能力与局限性,如不同优化级别及妨碍优化的因素;接着说明消除循环低效率、减少过程调用、消除不必要内存引用等优化手段,还指出部分优化效果可能受处理器设计影响,需不断尝试。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前言

有些时候,我们明明感觉自己的代码写出来十分合理且正确,但是在运行之后,却发现自己的代码的效率并没有意料中的高。其实,要想编写出高效的代码有很多因素要考虑,其中很重要的就是理解优化编译器的能力和局限性,同时尝试从系统方面提高代码性能。


提示:以下是本篇文章正文内容

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

大多数编译器会提供其所使用的优化的控制。最简单的控制就是指定优化级别。

  1. “-Og”调用GCC是让GCC使用一组基本优化。
  2. “-O1”或者更高的“O2”,“O3”是更大量的优化。可以进一步提高性能,但是可能会在编译阶段增加时间。

有时候我们可能会发现,即使是用 O1选项编译得到的代码,也比更高的优化编译等级编译出来的代码性能更高。

同时要注意的是,编译器必须很小心地对程序使用安全的优化,在优化时候遇到妨碍优化的因素大致可以分为:

  1. 内存别名使用
void swap(long *xp,long *yp)
{
	*xp = *xp + *yp;
	*yp = *xp - *yp;
	*xp = *xp - *yp;
}

上面这段代码中,如果xp等于yp,则会使得xp和yp同时改变,而一般不相等的情况,产生变化的只是xp或者yp其中一个值。

  1. 函数调用
long f();

long func1(){
	return f() + f() +f() + f();
}

long func2(){
	return 4*f();
}	

看上去func1和func2都是计算的同一值,但是当f代码如下时

long conter = 0;

long f(){
	return counter++;
}

每次调用返回的值都会不一样,所以最终结论,func1和func2肯定也会不一样。

大多数编译器不会试图去判断一个函数是否会有副作用,如果没有的话,就可能会被优化成func2的样子。当然大多会假设最糟糕的情况,不会进行优化。这正是优化编译器的局限性所在

二、消除循环的低效率

循环中一些每次都重复计算而又不产生改变的值,我们可以将其移除循环,将该值用一个局部变量代替,会取得良好效果。
如下面代码例子所示:

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

该示例中的vec_length(v)调用一次赋值给一个局部变量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;
 		}
 }

这个优化是一类常见的优化的一个例子,称作代码移动。这类优化包括要执行很多次识别但是计算结果不会产生改变的计算。

三、减少过程的调用

在上面提到的代码段combine1和combine2都会调用一个函数get_vec_element来获取下一个向量元素。我们尝试将其进行优化,用一个get_vec_start获取数组首地址,然后之后的向量元素采用访问数组的形式。
代码段如下所示:

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

但是经过测试,性能没有明显提升反而使得整数的求和性能略有下降。显然,内循环中其他操作形成了瓶颈,限制性能超过调用get_vec_element。
具体情况如下:
在这里插入图片描述
即使这个转变消除了每次迭代中用于检查向量索引是否在界限内的两个条件语句。但是对于这个函数来说,这些检测总是确定索引在界内的,所以是高度可预测的。它只会在最后一次导致预测错误处罚。

四、消除不必要的内存引用

combine3的代码将合并计算的值累积于指针dest指定的位置。查看了汇编代码。如下所示:
在这里插入图片描述
指针dest的地址存放在寄存器%rbx中,每次迭代的时候,累积变量的值都要从内存中读出再写入到内存。这样的读写很浪费。为了消除不必要的内存引用,我们可以引入一个临时变量acc,用以累积计算出的值,再for循环完成之后存放在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;
 }

与combine3相比,我们每次迭代的内存操作,从两次读一次写,变成了一次读,减少了内存的调用。
我们看到程序性能有了显著提高,如下表所示:
在这里插入图片描述

总结

我们使代码看起来像按照某种特殊顺序,对代码进行一系列的转换的简单线性过程。我们需要用系统的思维去理解去判断计算机怎么执行代码,以及仔细探寻代码中可以优化的方法。当然,一些看起来可行的代码,实际上并没有那么有效,正如上述的combine3代码。性能可能依赖于处理器设计的诸多细节,我们对此了解很少,所以需要不断学习,去尝试各种技术,做到程序的优化。
以上就是我对于从系统方面优化程序性能的几个简单方法的学习总结。

参考书籍

《深入理解计算机系统》 原书第三版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值