写程序的主要目标是使它在所有可能的情况下都能正确运行(bug free),一个运行得很快但有 bug 的程序是毫无用处的。在 bug free 的基础上,程序员必须写出清晰简洁的代码,这样做是为了今后检查代码或修改代码时,其他人能够读懂和理解代码。另一方面,让程序运行得更快也是一个很重要的考虑因素。不过,程序获得最大加速比的时候,是它第一次运行起来的时候。
在提到优化程序性能时(Code optimization),我们往往会想到算法与数据结构中的一个概念——复杂度。事实上,除了算法复杂度之外,仍然有许多的代码书写小细节可以改进性能表现。不过,编写高效的程序,第一个考虑的还是选择一组合适的算法与数据结构,因为算法复杂度影响还是相当大的,而且通常比其他常量因素更重要。第二点,我们必须写出编译器能够有效优化以转换成高效可执行代码的源代码。对于第二点,理解程序是如何被编译和执行、理解处理器和存储器系统是如何运作的、理解编译器优化的局限性是很重要的。在程序开发过程中,程序员必须在实现和维护程序的简单性与它的运行速度之间做出权衡,也就是在尽量不破坏程序的模块化和通用性的前提下,做到对代码性能的优化。
即使是最好的编译器也受到妨碍优化的因素(optimization blocker)的阻碍,程序员必须编写容易优化的代码,来帮助编译器(很让人眼界一新的观点)。研究程序的汇编代码,是理解编译器,理解代码如何被运行的最有效的手段之一。
理解编译器优化能力的局限性
编译器必须很小心地对程序使用安全的优化。在C语言标准的保证之下,编译器要确保优化后得到的程序和未优化的版本有着一样的行为(知道这个也就知道编译器不是万能的)看看下面两个过程:
void func1(int *xp, int *yp)
{
*xp += *yp;
*xp += *yp;
}
void func2(int *xp, int *yp)
{
*xp += 2* *yp;
}
乍一看这两个过程似乎有相同的行为,且过程 func2 的效率更高一点,它虽然用到了乘法,但只需要三次存储器引用(读 *xp
,读 *yp
,写 *xp
),而 func1 要用到六次存储器引用。那么编译器能不能把代码 func1 优化成 func2 呢?答案是否定的。当 xp 等于 yp 的情况下,func1 会把指针所指向的值增加 4 倍,而 func2 只会增加 3 倍。这就是一个优化前后程序行为不同的典型例子——两个指针指向同一个存储器位置的情况,叫做存储器别名使用(memory aliasing),这就造成了一个妨碍优化的因素——编译器不能确定两个指针是否指向同一个位置,那么它就必须假设可能会存在这种情况,限制了优化能力。所以程序员要编写帮助编译器的代码,帮助编译器产生高效的可执行代码。
代码移动
如果一个表达式总是得到同样的结果,最好把它移动到循环外面,这样只需要计算一次。编译器有时候会试图尝试代码移动,不过编译器会十分小心,它们不能确定移动一个函数的代码是否会有副作用,因此往往会假设会有副作用。所以程序员要手动帮助编译器来优化。
void set_row(double *a, double *b, int i, int n){
int j;
for (j = 0; j < n; j++){
a[n*i + j] = b[j];
}
}
// 这里 n*i 是重复被计算的,可以放到循环外面
void set_row(double *a, double *b, int i, int n){
int j;
int ni = n * i;
for (j = 0; j < n; j++){
a[ni + j] = b[j];
}
冗余的过程调用
看一个循环低效率,但编译器没办法优化的极端例子。下面这个函数的作用是将一个字符串中所有大写字母转换成小写字母
void my_lower(char *s)
{
int i;
for(i = 0; i < strlen(s); i++)
if(s[i] >= 'A' && s[i] <= 'Z')
s[i] -= ('A' - 'a');
}
这段代码的问题在于,每次循环都要调用一遍 strlen
。而strlen
的实现基本类似于下面这个样子:
int strlen(const char *s)
{
while(*s != '\0'){
s++;
length++;
}
return length;
}
在理想情况下,我们可能认为编译器能够认为循环中的 strlen
每次都会返回相同的结果,因此能够将其优化,移出循环。然而很可惜的是,这样的分析远远超出了编译器的能力。很多时候只能靠程序员自己进行代码优化。每次调用 strlen
就是一次 O(n),n是字符串长度。my_lower
的时间复杂度高达 O(n^2)。所以,一个看上去无足轻重的代码片段可能隐藏的渐进低效率。冗余的过程调用在字符串长度较低时毫无危险,但当应用到一个有一百万个字符的串上,突然,这段无危险的代码就会成为主要的性能瓶颈。
优化的方法就是让其计算一次就好:
void my_lower(char *s)
{
int i;
int len = strlen(s);
for(i = 0; i < len; i++)
if(s[i] >= 'A' && s[i] <= 'Z')
s[i] -= ('A' - 'a');
}
消除不必要的存储器引用
// 把 nxn 的矩阵 a 的每一行加起来,存到向量 b 中
void sum_rows1(double *a, double *b, int n)
{
int i, j;
for (i = 0; i < n; i++)
{
b[i] = 0;
for (j = 0; j < n; j++)
b[i] += a[i*n + j];
}
}
对应的汇编代码为
# sum_rows1 的内部 for 循环
.L4:
movsd (%rsi, %rax, 8), %xmm0 # 从存储器位置 b[i] 载入浮点数,%rsi 保存数组 b 的起始地址, %rax 保存 i
# %rdi 是 a[i*n+j] 的位置
addsd (%rdi), %xmm0 # 计算结果,放到%xmm0 是存放浮点数的寄存器
movsd %xmm0, (%rsi, %rax, 8) # 再把计算结果写会存储器位置
addq $8, %rdi
cmpq %rcx, %rdi
jne .L4
可以看到,每次都会把 b[i]
读入,写。但每次读入的时候,都是上次最后写入的值,这样的无用读写显得很浪费。我们能够消除这样的无用读写,引入一个临时变量,用来在循环中累计计算出来的值。只有在循环完成之后,才将结果写入存储器。
void sum_rows2(double *a, double *b, int n)
{
int i, j;
for (i = 0; i < n; i++)
{
double val = 0;
for (j = 0; j < n; j++)
val += a[i*n + j];
b[i] = val;
}
}
处理条件分支
在汇编语言的跳转时有说到,对于以流水线模式工作的CPU,遇到分支的时候,CPU必须预测分支往哪个方向走。如果预测失误,会导致很严重的性能惩罚。对于本质上无法预测的情况,如果编译器能产生使用条件数据传送而不是条件控制转移的代码,能极大提高程序的性能。
void minmax1(int a[], int b[], int n){
int i;
for(i = 0; i < n; i++)
{
int t = a[i];
a[i] = b[i];
b[i] = t;
}
}
//优化版本如下:
void minmax2(int a[], int b[], int n){
int i;
for(i = 0; i < n; i++)
{
int min = a[i] < b[i] ? a[i] : b[i];
int max = a[i] < b[i] ? b[i] : a[i];
a[i] = min;
b[i] = max;
}
}
参考链接
【CSAPP笔记】10. 代码优化的更多相关文章
-
操作系统概念学习笔记 10 CPU调度
操作系统概念学习笔记 10 CPU调度 多道程序操作系统的基础.通过在进程之间切换CPU.操作系统能够提高计算机的吞吐率. 对于单处理器系统.每次仅仅同意一个进程执行:不论什么其它进程必须等待,直到C ...
-
thinkphp学习笔记10—看不懂的路由规则
原文:thinkphp学习笔记10-看不懂的路由规则 路由这部分貌似在实际工作中没有怎么设计过,只是在用默认的设置,在手册里面看到部分,艰涩难懂. 1.路由定义 要使用路由功能需要支持PATH_INF ...
-
《C++ Primer Plus》学习笔记10
<C++ Primer Plus>学习笔记10 <<<<<<<<<<<<<<<<<&l ...
-
SQL反模式学习笔记10 取整错误
目标:使用小数取代整数 反模式:使用Float类型 根据IEEE754标识,float类型使用二进制格式编码实数数据. 缺点:(1)舍入的必要性: 并不是所有的十进制中描述的信息都能使用二进制存储,处 ...
-
JAVA自学笔记10
JAVA自学笔记10 1.形式参数与返回值 1)类名作为形式参数(基本类型.引用类型) 作形参必须是类的对象 2)抽象类名作形参 需要该抽象类的子类对象,通过多态实现 3)接口名为形参 需要的是该接口 ...
-
golang学习笔记10 beego api 用jwt验证auth2 token 获取解码信息
golang学习笔记10 beego api 用jwt验证auth2 token 获取解码信息 Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放 ...
-
Spring MVC 学习笔记10 —— 实现简单的用户管理(4.3)用户登录显示全局异常信息
</pre>Spring MVC 学习笔记10 -- 实现简单的用户管理(4.3)用户登录--显示全局异常信息<p></p><p></p>& ...
-
Python标准库笔记(10) — itertools模块
itertools 用于更高效地创建迭代器的函数工具. itertools 提供的功能受Clojure,Haskell,APL和SML等函数式编程语言的类似功能的启发.它们的目的是快速有效地使用内存, ...
-
Hadoop学习笔记(10) ——搭建源码学习环境
Hadoop学习笔记(10) ——搭建源码学习环境 上一章中,我们对整个hadoop的目录及源码目录有了一个初步的了解,接下来计划深入学习一下这头神象作品了.但是看代码用什么,难不成gedit?,单步 ...
-
强化学习读书笔记 - 10 - on-policy控制的近似方法
强化学习读书笔记 - 10 - on-policy控制的近似方法 学习笔记: Reinforcement Learning: An Introduction, Richard S. Sutton an ...
随机推荐
- MyBatis操作指南-搭建项目基础环境(基于Java API)含log4j2配置
-
Mac OS X Tips
命令行查看Mac OS X版本 $ sw_vers ProductName: Mac OS X ProductVersion: BuildVersion: 14D131 Mac OS X截图 不要使用 ...
-
清除PDF里的元数据和机密信息的方法
相信很多人都知道,PDF文档的表现形式可以大不相同,它们可能包含某些数据,乍一看根本看不见,那些数据可能是不适合共享的信息-比如元数据(作者.主题.关键词).书签.扫描文档里的文本层等,通过ABBYY ...
-
gVim 配置方案 采用Vundle管理插件
在Linux下配置vim非常简单,尤其是采用Vundle来管理插件,使得一切用起来得心应手. Maple大神在github上公布了自己的vim配置方案,相当方便好用.详见 https://github ...
-
使用graphics2D给图片上画字符
//读取图片BufferedImage frontImage = ImageIO.read(new File(eCardXMLConfigManager.geteCardXMLConfigManage ...
-
【转】UILabel、UITextView自适应得到高度
原文:http://blog.csdn.net/xcysuccess3/article/details/8331549 在iOS中,经常遇到需要根据字符串的内容动态指定UILabel,UITextVi ...
-
Oracle优化技术
1.基本原理 Oracle的日志:Oracle中为了提高硬盘写的效率,採用内存中数据缓冲区来保存数据,等到一定量或一定时间后才写到磁盘(DBWR). 这个时候假如断电之类的故障发生,数据缓冲区的数据将 ...
-
Nodejs.热部署方法
在开发中我们修改了一点代码后要去重启服务器才能看到结果,为了省去这个过程我们以往经常使用热部署代码的方法 下面是使用"supervisor"来达到热部署能力的方法: sudo np ...
-
Mybatis逆向工程自动生成代码(Ubuntu18.04-idea环境)
最近在学习taotao商城项目,有一节是关于mybatis逆向工程的,参考了这个博文,https://blog.csdn.net/yerenyuan_pku/article/details/71909 ...
-
WPF中使用TextBlock的Inlines属性来完成复杂的文字内容
参考:http://blog.csdn.net/zhangjiyehandsom/article/details/5498845 1. 需求:要在一行内容中显示不同颜色以及粗细不一的字体, 解决办法: ...