在前两篇文章里,我们讨论了程序性能的两个方面,一是算法(广义的算法,即解决问题的方法),二是编译器。通过这两个方面,我想表达的意思是,一段程序的执行效率,是很难从表面现象得出结论的,至少从一些简单的层面,如代码的长度是几乎难以说明任何问题——因此一定要进行Profiling才能做到有效的优化。而现在,我们假设两段程序算法基本相同,编译器也只是进行简单的“翻译”,那么……我们能从“表面”看出性能高下吗?

那么就从一个最简单的例子看起吧。假设DoSomethingA和DoSomethingB里做的事情是固定的,那么您认为下面两种写法的哪个性能更好?

for (int i = 0; i < 100; i++)
{DoSomethingA();DoSomethingB();
}
for (int i = 0; i < 100; i++)DoSomethingA();for (int i = 0; i < 100; i++)DoSomethingB();

这两段逻辑的算法基本上完全相同,如果编译器只是进行直接“翻译”而不进行优化,那么第一种做法对于i的累加和条件跳转比较少,因此您可能会得出结论:“很明显”第一段代码的执行效率比较高。只可惜事实并非那么简单,因为影响程序性能的另一个关键因素是:缓存。

“缓存”无处不在。在CPU中,性能最快的存储设备当属“寄存器”,不过众所周知寄存器的数量是极其有限的。因此,CPU都会有L1 Cache和L2 Cache的多级缓存机制。其中,L2 Cache的性能比L1 Cache和寄存器都要慢,但还是比内存要快许多。当某个Core需要从内存中获取数据的时候,便会从L1 Cache获取数据,如果L1 Cache没有那么就会从多个核共用的L2 Cache拿,再没有便会从内存拿——由于操作系统的虚拟内存机制,可能还要从磁盘的交换页中获取数据,此时性能自然相当差了。

虽然寄存器只使用一个字长(如4字节)的数据,但是L1 Cache从L2 Cache拿数据时总是“一块一块”拿的——这么一块往往就是连续的64个字节。换句话说,在CPU读取的一个地址的数据之后,读取其他一些地址上的数据便会比另一些特别快,因为它们都已经在L1 Cache中了。如果一个程序能够利用起CPU的这个特性,那它的性能往往便可以更好一些(自然还有很多其他影响性能的因素)。

局部性(Locality),便是用来描述程序是否能利用好缓存的名词。我们说一个程序的局部性比较好,那么就表示它能够较好地利用起CPU的缓存机制。局部性分“空间局部性”和“时间局部性”两方面,前者是指“加载一个地址的数据之后,继续加载它附近的数据”,后者表示“在加载一个地址的数据之后,短时间内重新加载这块数据”。无论是哪一方面,目的都是希望从较快的缓存中加载“热”的数据。为什么冷启动总是很慢?为什么有人说系统从开机后会越跑越快?其实道理都差不多。

那么现在,您还能判断上面两种做法的效率孰高孰低?虽然第一种做法减少了i的累加次数和条件跳转的次数,但是它在一次循环中做了两件事情,可能在执行DoSomethingB方法的时候,DoSomethingA方法中刚刚进入缓存的数据便冷却了,于是在下次执行DoSomethingA时又要重新从较慢的存储设备中加载数据。而在第二种做法中,我们“密集”地执行完100次DoSomethingA或DoSomethingB的调用,而此间大量的数据访问都是集中在L1 Cache上,性能优势不言而喻。

我以前的文章《计算机体系结构与程序性能》在第一部分里也讨论了局部性对程序性能的影响,讲的更为具体一些,您也可以参考其中的内容。

由于程序指令不是执行效率的唯一因素,因此从代码长短上判断程序性能也是非常不靠谱的事情。当然,从任何独立的角度来判断性能可能都不合适。例如在那篇文章里提到,出于程序性能的考虑应该使用全局变量——当然作者也认为这不是好的设计,事实上在我们刚才的例子中,在一个循环中做多件事情可能也值得重构。如果您使用全局变量,它的确省下了push,pop等指令的开销,但是这么一个全局变量——例如是一个静态变量,它存储在堆的某一个地方,访问它并非是一个局部性方面的优秀实践。与之相反,由于L1 Cache的作用,在调用栈上访问“参数”或“局部变量”并不会比访问寄存器慢多少,此时push,pop几个指令的开销可能就不算什么了。更何况,如果编译器/运行时内联了这个方法,这样连push,pop等指令也不会出现了。

记得前一段时间在有某些朋友在我的博客上发布一些较为“激进”的说法,例如“学底层只是对写.NET程序没有帮助,因为就算你知道了这些,C#也没有办法内嵌汇编”。我不同意这个说法,因为即便是.NET程序,它也是在符合计算机体系结构的规律下运行的,我们完全可以在一定程度上了解一段代码在执行时的表现。

就拿目前谈到的“局部性”来说,我们便可以把握很多东西。比如,我们知道每个线程的调用栈在默认情况下是1兆大小,因此两个线程调用栈上的数据几乎不可能出现在同一个Cache条目中。再比如,由于“时间局部性”,最近使用的数据最有可能出现在缓存中,因此在.NET 4.0的并行库在调度“私有队列”的任务时会倾向于执行最新创建的任务。再比如,您是使用两个int数组来表示一系列坐标的x值和y值,还是构造一个struct Point数组来保存它们呢?虽然使用两个int数组更节省内存,但是从局部性考虑问题的话,您会发现同一个坐标的x值和y值存放在一起可能更为合适。

我的这几篇文章,其实也都在强调从代码表面判断程序性能的“不确定性”。同样道理,即便是把它们的汇编代码(片断)放在您面前,您也可能很难“看出”性能区别。这也从侧面说明了Profiling的重要性:阅读代码是静态的,而程序执行和Profiling都是动态的。之前有朋友对我说“你最近迷上Profiler啦?”其实我这里的Profiling泛指“一种探索程序性能的方式”,并不是指某个特定的手段,更不是某个具体的工具——不过无论是使用VS的Profiler也好,还是自己搞一个CodeTimer,都比“读代码”来的可靠。

from: http://blog.zhaojie.me/2010/01/talk-about-code-performance-3-locality.html

浅谈代码的执行效率(3):缓存与局部性相关推荐

  1. 浅谈代码的执行效率(4):汇编优化

    终于谈到这个话题了,首先声明我不是汇编优化的高手,甚至于我知道的所有关于汇编优化的内容,仅仅来自于学校的课程.书本及当年做过的一些简单练习.换句话说,我了解的东西只能算是一些原则,甚至也有一些&quo ...

  2. 浅谈代码的执行效率(2):编译器的威力

    在上一篇文章中,我主要表达了这样一个观点:影响程序效率的关键之一是算法,而算法的选择与优化,和是否多一个赋值少一个判断的关系不大.关于算法的选择,我谈到其理论上的复杂度,并不直接反映出效率.因为在实际 ...

  3. 浅谈代码的执行效率(2):编译器的威力 [摘自赵劼老师的博客]

    在上一篇文章中,我主要表达了这样一个观点:影响程序效率的关键之一是算法,而算法的选择与优化,和是否多一个赋值少一个判断的关系不大.关于算法的选择,我谈到其理论上的复杂度,并不直接反映出效率.因为在实际 ...

  4. 浅谈代码的执行效率(1):算法是关键

    前一段时间在博客园里看到这样一篇文章,那位兄弟谈到程序效率的关键是"简短".他说,"程序越简短,其可执行代码就越少,就越有效率",而在编写程序的时候," ...

  5. java缓存同步_浅谈JSON的数据交换、缓存问题和同步问题

    JSON轻量级的数据交换格式 相对于XML来说,JSON的解析速度更快,文档更小. JSON的格式 {属性名:属性值,属性名:属性值,--} 属性名的类型可以是string,number,boolea ...

  6. android onclick执行顺序,浅谈onTouch先执行,还是onClick执行(详解)

    有一个Button 按钮,要想为该按钮设置onClick事件和OnTouch事件 mTestButton.setOnClickListener(new View.OnClickListener() { ...

  7. 浅谈提升C#正则表达式效率

     摘要:说到C#的Regex,谈到最多的应该就是RegexOptions.Compiled这个东西,传说中在匹配速度方面,RegexOptions.Compiled是可以提升匹配速度的,但在启动速度上 ...

  8. 掌握这35 个小细节,助你有效提升 Java 代码的执行效率!

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 作者:萌小Q 来源:https://www.cnblogs.com/Qian123/p/60 ...

  9. var和function谁先优先执行_浅谈JavaScript 的执行顺序

    JavaScript是一种描述型脚本语言,它不同于java或C#等编译性语言,它不需要进行编译成中间语言,而是由浏览器进行动态地解析与执行.如果你不能理解javaScript语言的运行机制,或者简单地 ...

最新文章

  1. 微调也重要:探究参数初始化、训练顺序和提前终止对结果的影响
  2. PAT (Basic Level) Practice (中文)1014 福尔摩斯的约会 (20 分)
  3. 面试项目亮点_程序员面试时这样介绍自己的项目经验,等于成功了一大半
  4. 读书日记 莫雨 《一个程序员的奋斗史》Java 面试 感悟 程序员
  5. Java多线程学习九:如何正确关闭线程池?shutdown 和 shutdownNow 的区别
  6. php设置session 生命周期,php会话(session)生命周期概念介绍及设置更改和回收
  7. K8S 部署rabbitmq集群
  8. [leetcode]208. 实现 Trie (前缀树)
  9. STL:STL各种容器的使用时机详解
  10. Java Swing Action 动作
  11. QT、C++面试中的几个问题
  12. 飞腾服务器虚拟化,基于飞腾平台的容器虚拟化技术研究
  13. 元胞自动机概念与实例
  14. 【很赞的一片文章】android获取手机号码(主要是移动手机)
  15. Redis使用pipeline批量查询所有键值对以及multiGet用法
  16. 2020ICPC昆明热身赛 C.Statues(前缀优化dp+滚动数组优化空间)
  17. 图纸上标注的是实际尺寸吗_尺寸数字应该标注图纸上所画实际长度。
  18. CRX文件安装Chrome/chromium版Edge上的方法
  19. 关于socket error 10054
  20. 计算机配件模拟,电脑装机模拟各配件跑分及计算公式分享

热门文章

  1. Oracle-使用切片删除的方式清理非分区表中的超巨数据
  2. php print_r this,PHP 打印函数之 print print_r
  3. 创建一个类 new 与 不加new 有什么区别?
  4. getPath()和getResource()找不到文件(NullPointerException)的原因(idea创建properties文件)
  5. androidx使用FileProvider适配安卓7
  6. halcon算子盘点:Chapter 11 :Morphology1
  7. python的目的及应用_python Django中的apps.py的目的是什么_python_脚本之家
  8. python生成100个随机数_Python_0——100闭区间产生3个随机数,两种方法排序
  9. 基于MATLAB的OFDM系统实现
  10. Spring学习6之自动装配Bean02