【CSAPP笔记】10. 代码优化
写程序的主要目标是使它在所有可能的情况下都能正确运行(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 ...
随机推荐
- Redis高级特性---------事务与持久化与发布订阅
一.redis事务的用法 1.开启事务:multi 2.提交事务:exec ( queued只是把指令放入队列中,没有执行) 3.取消事务:discard 4.redis事务不能保证同时成功或者失 ...
- redis缓存数据库入门教程
入门redis教程 前言: 应公司需求,最近学习了一下redis数据库的一些简单入门的教程,整理出来分享给大家,喜欢的可以关注和点赞哦~ 如文章中有不足之处求指正,谢谢 目录 ·什么是redis?为什 ...
- JavaWeb基础—HttpServletResponse
HttpServletResponse对象代表服务器的响应. 这个对象中封装了向客户端发送数据.发送响应头,发送响应状态码的方法. 几个方法: 向客户端发送数据: getOutputStream() ...
- 不能存在多个@ManyToMany(fetch=FetchType.EAGER)
@LazyCollection(LazyCollectionOption.FALSE) 原地址:https://stackoverflow.com/questions/4334970/hibernat ...
- 【HNOI2013】消毒
题面 题解 当只有二维时,就是一个二分图匹配的板子题 三维的时候就很好做了,暴力枚举一维的情况,因为\(\min(x,y,z) = \sqrt{5000} < 18\),于是时间复杂度有保证 代 ...
- Wannafly挑战赛5 D. 子序列
D. 子序列 链接 #include<cstdio> #include<algorithm> #include<cstring> #include<iostr ...
- 无聊中,写个常见的图片保护分类-iOS
当美工给出例如下面这种图的时候,但是需要拉伸的时候,就不得不做拉伸保护了. //调用代码 UIImage * img = [UIImage protectedImageWithLocalImageNa ...
- 并行Linq
有时候我们对大批量数据进行处理,此时并行linq就起作用了. 并行查询 对于以下查询可以耗时会非常大,如下: ; var r = new Random(); , arraySize).Select(x ...
- 【译】2017年要学习的三个CSS新特性
这是翻译的一篇文章,原文是:3 New CSS Features to Learn in 2017,翻译的不是很好,如有疑问欢迎指出. 新的一年,我们有一系列新的东西要学习.尽管CSS有很多新的特性, ...
- curl命令实现上网认证登录
为了想让组里的服务器连外网下数据,需要命令行上网登录.与很多高校的上网方式一样,大气所上网采用的是用户帐号登录验证的方法.上网需要需要先开浏览器,然后打开网页输入帐号密码登录.参考了前人的一些帖子,最 ...