“你的时间有限,所以不要为别人而活。不要被教条所限,不要活在别人的观念里。不要让别人的意见左右自己内心的声音。最重要的是,勇敢的去追随自己的心灵和直觉,只有自己的心灵和直觉才知道你自己的真实想法,其他一切都是次要。 ——史蒂夫·乔布斯”

CSAPP的第五章“优化程序性能”,从机器底层的角度阐述了如何去优化。说实话,这章就应该撕掉,然后扔进垃圾桶。真是越看越火大,越看越觉得扯淡。要是你想成为一个三流程序员,就应该一丝不苟地按照书中的做。

如果你写了个程序,觉得它太慢。那么你可以花半年去优化它,也可以跑去找小姑娘玩半年回来,然后更强大的硬件就会让你的程序更快。

优化仅仅是在万不得已之时才应该去做,只要程序能工作,还能忍受它的速度,何必要去优化。程序员的时间宝贵,牺牲机器的时间,换取程序员的轻松时光不是天经地义的事吗!想当年编程是要在纸带上打孔输入机器的,所以后来有了汇编,又后来有了C,再后来有了Python。每一层级的递进都让程序员的开发效率得到了进一步提高,同时稍微牺牲了点机器时间。

要记住:程序员的时间远比机器时间宝贵。

下面就说说为什么不应该按书中的进行优化。

测试机器:

CPU Intel Core i5 M520 2.40GHz

RAM 4GB

Windows 7

举些例子来说明问题:

1)消除不必要的存储器引用

有如下两个累加函数,ret为返回的地址参数

sum1:

   1:  void sum1(int *a, int len, int *ret)
   2:  {
   3:      *ret = 0;
   4:   
   5:      for(int i = 0; i < len; i++)
   6:          *ret += a[i];
   7:  }

sum2:

   1:  void sum2(int *a, int len, int *ret)
   2:  {
   3:      int s = 0;
   4:   
   5:      for(int i = 0; i < len; i++)
   6:          s += a[i];
   7:   
   8:      *ret = s;
   9:  }

两个函数的功能是完全一样的,而且sum1也来得更直观些,sum2有时就会让人不解为何要引入一个中间变量s来保存累加和。原因就是,你得先把两段代码反汇编:

sum1片段:

movl (%edi), %eax

imull (%ecx, %edx, 4), %eax

mov %eax, (%edi)

sum2片段:

imull (%ecx, %edx, 4), %eax

看懂了没,sum1中对ret的解引用会导致从%edi (ret) 的地址中取值(*ret)赋值给%eax,然后再从%eax赋值回(%edi)这个过程。而循环len次就会多出2 * len条指令。所以sum2的效率要高,那么高多少呢,如下图:

注意,这里的时间单位是ms,1000ms=1s。当len的长度不断以10的数量级递增时,sum2的时间优势其实并不明显。在len = 10^8时,差距仅仅是140ms(加速因子k=1.34,k=sum1时间/sum2时间),这点加速实在是太少了。况且由于现代硬件速度的提升,这种差异在以后也会越来越小。

2)循环展开技术(Loop Unrolling)

依然是对sum1函数的进一步优化,于是有了邪恶的sum3:

   1:  void sum3(int *a, int len, int *ret)
   2:  {
   3:      int s = 0;
   4:      int limit = len - 2;
   5:   
   6:      int i;
   7:      for(i = 0; i < limit; i += 3)
   8:          s += a[i] + a[i + 1] + a[i + 2];
   9:   
  10:      for( ; i < len; i++)
  11:          s += a[i];
  12:   
  13:      *ret = s;
  14:  }
 

len = 10^8时,sum3相对于sum2提高了172ms(k=1.74),相对sum1提高312ms(k=2.33)。为什么会有这种提高?可以看到在循环中步长变为了3,那么为什么要是3而不是其他呢?

要讲清楚就不得不先讲讲这幅图:

这幅图是什么呢?就是讲在一个理想的国度,世界上的资源是无穷无尽的,包括计算机中的硬件资源。于是呢,每个周期我们都有无限的计算器件可用。看到在第三周期用到了jl, compl, incl的硬件资源,而这三个硬件其实都是要用到加法器的。因为有无限的器件所以没事,同时此时效率当然是最高的。

可是,现实总是很残酷的。漂亮小姑娘总是有限,计算机的硬件也不可能无限多。神告诉我们,你只能有两个加法器,不可太贪。于是在同一周期,我们只能有两个涉及加法的指令。那么就有了如下的现实中的计算版本。显然效率要比理想版本差,效率拖后约1倍。

很自然的,如何用手头有限的资源创造最大的财富,就是我们关心的。看到load指令的周期是一般指令的3倍,而load是可以流水执行的,那么尽量让load干活就是我们所期望的。如何改进呢?你可能想到了。邪恶的sum3版本登场:

图中目的很明显,尽量减少addl, compl和jl(ACJ)这些指令的执行,这样就不至于太受加法器资源的限制,另外尽量利用load的流水特性。那么如何减少ACJ的执行呢?终于想到了加大每次步长了吧。又为什么是3呢?想到了load的周期是3了吧。谜团终于解开,看看最终版本的sum3是如何在机器中执行的:

看到3步一循环的方法能有效地降低ACJ的执行,同时充分地利用了load的流水特性。最后,这个版本相对理想版本效率拖后约0.33倍。

既然能增加性能又为什么要谨慎呢?问题是,以后呢?再以后呢?让我们看得远一点,再远一点。硬件的发展总是如此之快,加法器会有的,load会更快的。然后呢,然后就是,你做的优化还得随着硬件不断修正,几个月后当你或者其他人重新审视你的代码时,你或他都不知道为什么这家伙写了这么恐怖的代码。于是,一切必须推倒重来。代码根本不具可维护性。

程序首先是写给人看的,然后顺便让机器读懂。

3)循环分割(Loop Splitting)

继续sum1的讨论,如果我们把加法操作改为乘法呢?

   1:  void multi1(int *a, int len, int *ret)
   2:  {
   3:      *ret = 1;
   4:   
   5:      for(int i = 0; i < len; i++)
   6:          *ret *= a[i];
   7:  }

书中再次给出了一个诡异的优化:

   1:  void multi2(int *a, int len, int *ret)
   2:  {
   3:      int limit = len - 1;
   4:      int m0 = 1;
   5:      int m1 = 1;
   6:   
   7:      int i;
   8:      for(i = 0; i < limit; i += 2)
   9:      {
  10:          m0 *= a[i];
  11:          m1 *= a[i + 1];
  12:      }
  13:   
  14:      for( ; i < len; i++)
  15:          m0 *= a[i];
  16:   
  17:      *ret = m0 * m1;
  18:  }

m0计算偶数下标的乘积,而m1计算奇数下标。最后合并。看看到底有多快:

Len= 10^8,差异140ms, k=1.43,加速实在有限,而且还是在数据规模这么大的时候。另外,由于之前的loop unrolling技术也基本上能猜出个大概了。乘法器件耗费的周期巨大,又由于其流水特性。所以考虑增加每次循环中的乘法次数,而不必等到乘积结果迭代到下次而产生的延时。

这又是一个极度依赖特定机器的优化。这里我们可以猜到有2路并行,必定也能有3路,4路……真是没完了。

这样的优化严重地破坏了程序的优雅性。

其他

另外还有其他的一些比较小方面的,同时让人不爽的优化。

1) 将数组版本转换成指针版本有时会快一点(真的是没那么大区别,同学爱怎么写就怎么写,指针有时真是万恶之源!)

2) 将sum1中的len显示的放入lenReg中(即:int lenReg = len),为什么呢?因为len被调用时是从栈中取出的,这样显示地放入放入一个寄存器中,省去了循环中每次从内存中取值的过程。(能差多少呢!?)

3) 另外小心地设置乘法顺序也能提高程序性能哦!看如下的乘法:

r = ((r * x) * y) * z; //(a)

r = (r * (x * y)) * z; //(b)

r = r * ((x * y) * z); //(c)

r = r * (x * (y * z)); //(d)

r = (r * x) * (y * z); //(e)

知道哪个最快吗?是(c)和(d),想知道为什么吗?我都懒得讨论如此无聊的问题了。往那该死的并行性和流水性考虑吧。

硬币总是有两面的。喷了这么久,还是回到这章的一些优点:1)减少对相同函数的调用,用一个变量保存返回值是一个好办法。2)对于隐性增加复杂度的系统函数,特别是在循环中的函数要注意。3)指针的调用所引起的一些意想不到的效果的注意事项。4)最后利用profile来分析程序瓶颈,着重优化瓶颈函数。5)Amdahl定理。

但这章中90%的东西都不应该去学习,学了就真的成了三流程序员了。优化程序首先应当关注其宏观方面:1)数据结构。2)算法。3)对问题刻画和建模的准确性。4)整体结构。5)优雅性。

最后也是最重要的一点:能不优化就别优化吧,有时间爱干嘛干嘛去。

CSAPP第五章就在“扯淡”!相关推荐

  1. CSAPP第五章家庭作业参考答案

    (CSAPP第三版系列)导航篇传送门 5.14编写5.13的6*1循环展开版本 代码如下: /* Inner product. Accumulate in temporary */ void inne ...

  2. 【玩转微信公众平台之一】序章(纯粹扯淡)

    昨天是我的生日,为了庆祝这一伟大的节日,我决定写个微信公众平台开发的系列教程. 看到这里有些人肯定迫不及待的要在下面的评论里写上"祝博主生日快乐"之类的祝福,其实我觉得大可不必,历 ...

  3. 教育|我在美国读博士才发现,美国高等教育如此残酷,以前的感觉完全是扯淡...

    如果说一百多年前,林则徐呼吁中国要睁眼看世界,那么现在我们国人仍需要透过迷雾看世界,不要被外国的种种假象所迷惑,不要再像生活在井底的青蛙一样低估自己没有看到的东西. 总之,我们的高等教育缺少了监督鞭策 ...

  4. #第五章“拷问”既往的股市理论5.1有人情味的“拷问”

    让谁说自己的短处,都不喜欢说,这是人情世故.所以只能上刑"拷问"了.特别是这些都是股市的老前辈,功成名就,况且,我所有的知识还都是从他们那里学来的,然后自己根据股市证据用数学的方法 ...

  5. 【游戏编程扯淡精粹】调试方法论

    [游戏编程扯淡精粹]调试方法论 文章目录 [游戏编程扯淡精粹]调试方法论 故障树分析法 Bug跟踪系统 & Bug交流分享 面对Bug的心态 如何在工作中正确高效地修bug? 找到原作者和模块 ...

  6. loop在计算机组成原理,计算机组成原理五章.ppt

    文档介绍: 5 处理机设计-数据路径和控制部件 哺忆箩店都嘛馆搞隔侯粹辣秃告***钾咳馁刘伞熙婿员萤涤煤拳撩砚妆缕斩计算机组成原理五章计算机组成原理五章 来辟津沥独同摇恰君卫艘虚潜惯龄彰毛堤占贴汀贰豁 ...

  7. 天龙日梅兰竹菊_第三百一十五章 梅兰竹菊

    第三百一十五章梅兰竹菊 自打应下无崖子的承诺以来,楚柏便是一直马不停蹄的赶路! 赶到西夏,在见了李秋水之后,又被李秋水拉着前往[缥缈峰],这一路,风尘仆仆的楚柏,总算是难得空闲下来了: 不得不说! [ ...

  8. 【游戏编程扯淡精粹】如何学习编程语言

    [游戏编程扯淡精粹]如何学习编程语言 文章目录 [游戏编程扯淡精粹]如何学习编程语言 如果你没有学过编程 如果你只是想提升工作效率 如果你想学习计算机科学与技术 如果你已经是熟悉了一门编程语言 如果你 ...

  9. 扫盲加扯淡——网友随笔画之云计算

    扫盲加扯淡--网友随笔画之云计算 今天在论坛看到网友自创的漫画,也许就是随笔画画吧, 且不说这位网友是否真的理解云计算(老实说,我也不清楚云计算是什么.),但确实是这位网友还是很有才的,能把自己的看法 ...

最新文章

  1. 【面向对象编程】(3) 类之间的交互,依赖关系,关联关系
  2. SlackTextViewController
  3. Aurora公式编辑器在64位Word 2013不显示选项卡
  4. Bootstrap3 过渡插件
  5. KMP算法(C语言版)
  6. Scala学习小小总结
  7. Jmeter --- Http Cookie Manager
  8. 分享个B端竞品分析报告
  9. [海森推荐] 人工智能:一种现代方法
  10. 干货——常用医药数据库
  11. GCT 英语单词分组记忆手册
  12. NLP数据集:GLUE【CoLA(单句子分类)、SST-2(情感二分类)、MRPC、STS-B、QQP、MNLI、QNLI、RTE、WNLI】【知名模型都会在此基准上进行测试】
  13. Rfb-Net(代码解读慢慢啃).
  14. 莫让“浮云”遮望眼:“企业技术”才是硬道理
  15. 小米手机安装linux视频教程,技术|在手机上轻松安装 Ubuntu Touch OS
  16. SQL Server - 数据库(创建,修改管理-删除)-T-SQL 语句
  17. Effie:真正的极简主义!秒杀幕布
  18. 有没有可以测试充电宝电流电压的软件,USB充电电流/电压检测仪USB电流和电压测试仪移动电源测试...
  19. 【笔记】多因素条件下注意力分配建模
  20. 2022出海中东:沙特阿拉伯电商市场现状及发展前景

热门文章

  1. TextSwitcher的使用(一)
  2. 题目1019:简单计算器
  3. DPDK KNI 接口2
  4. Graves of the Internet - 互联网坟墓
  5. iOS去除导航栏和tabbar的横线
  6. 证书过期--app审核提示90034证书错误
  7. EF6连接MySql,网上那些所谓解决方案提供者你们够了
  8. 扩展ToolBarManager、ListView和Grid控件以实现气球式的ToolTip
  9. PHP通过Thrift操作Hbase
  10. 利用Canny边缘检测算子进行边缘检测的原理及OpenCV代码实现