【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 ...
随机推荐
- 腾讯云Mac图床插件
背景 随着博客越写越多,难免会遇到需要插入图片来说明的情况. 图床选择 首先调研了市面上的图床服务,本着稳定长期的目标,过滤掉了打一枪换一个地方的野鸡小网站,剩余比较靠谱的优缺点如下. 图床 优点 缺 ...
- Scala_单例对象
在 Scala 中,是没有 static 这个东西的,但是它也为我们提供了单例模式的实现方法,那就是使用关键字 object. 对象的无参构造器在第一次使用时被调用,且单例对象没有有残构造器. Enu ...
- Linux 定时清除日志 Log
一.原因 写这篇的原因是项目中log没有定时清除,服务器上项目是用脚本启动,log文件只会在启动时生成一次,这时,由于项目在不断运行中,导致log越来越大.如果删除log文件,还得把项目停掉在启动,这 ...
- 使用Nginx+uWSGI+Django方法部署Django程序
第一步先解决uwsgi与django的桥接.解决在没有nginx的情况下,如何使用uwsgi+DJANGO来实现一个简单的WEB服务器. 第二步解决uwsgi与Nginx的桥接.通过nginx与uws ...
- Gitlab+Jenkins学习之路(十)之Jenkins按角色授权和Pipeline
一.Jenkins按角色授权 当一个公司的开发分为多个组别,或者是多个项目等等.用于公司内部测试,让开发人员自行构建测试,此时不可能让所有的开发都在公用一个构建,这样变得很混乱,为了解决这一问题,je ...
- 洛咕 P2494 [SDOI2011]保密
出题人没素质啊,强行拼题还把题面写得又臭又长. 简单题面就是有一张图,每条边有两个权值\(t,s\),有无限支军队,一支军队可以打一个点,代价是从n到这个点的路径的\(\frac{\sum t}{\s ...
- BZOJ3196 二逼平衡树 ZKW线段树套vector(滑稽)
我实在是不想再打一遍树状数组套替罪羊树了... 然后在普通平衡树瞎逛的时候找到了以前看过vector题解 于是我想:为啥不把平衡树换成vector呢??? 然后我又去学了一下ZKW线段树 就用ZKW线 ...
- 联赛emacs配置
(custom-set-variables ;; custom-set-variables was added by Custom. ;; If you edit it by hand, you co ...
- 对大表进行全表更新,导致 Replication 同步数据的过程十分缓慢
在Publisher database中更新一个big table,数据行数是3.4亿多.由于没有更新 clustered Index key,因此,只产生了3.4亿多个Update Commands ...
- CSS快速入门-后端布局
一.后台框架概述 我们在网上随便搜索后台框架,你会发现大部分都查不多.正所谓:好看的皮囊千篇一律,有趣的灵魂万里挑一. 第一个是H-ui,H-ui.admin是用H-ui前端框架开发的轻量级网站后台模 ...