学过计算机底层原理、了解过不少架构设计或者是作过优化的同窗,应该很熟悉局部性原理。即使是非计算机行业的人,在作各类调优、提效时也不得不考虑到局部性,只不过他们不经常使用局部性一词。若是抽象程度再高一些,甚至能够说地球、生命、万事万物都是局部性的产物,由于这些都是宇宙中熵分布布局、局部的熵低致使的,若是宇宙中到处熵一致,有的只有一篇混沌。
因此什么是 局部性 ?这是一个经常使用的计算机术语,是指处理器在访问某些数据时短期内存在重复访问,某些数据或者位置访问的几率极大,大多数时间只访问_局部_的数据。基于局部性原理,计算机处理器在设计时作了各类优化,好比现代CPU的多级Cache、分支预测…… 有良好局部性的程序比局部性差的程序运行得更快。虽然局部性一词源于计算机设计,但在当今分布式系统、互联网技术里也不乏局部性,好比像用redis这种memcache来减轻后端的压力,CDN作素材分发减小带宽占用率……
局部性的本质是什么?其实就是几率的不均等,这个宇宙中,不少东西都不是平均分布的,平均分布是几率论中几何分布的一种特殊形式,很是简单,但世界就是没这么简单。咱们更长听到的发布叫作高斯发布,同时也被称为正态分布,由于它就是正常状态下的几率发布,起几率图以下,但这个也不是今天要说的。 html
局部性有两种基本的分类, 时间局部性 和 空间局部性 ,按Wikipedia的资料,能够分为如下五类,其实有些就是时间局部性和空间局部性的特殊状况。java
若是某个信息此次被访问,那它有可能在不久的将来被屡次访问。时间局部性是空间局部性访问地址同样时的一种特殊状况。这种状况下,能够把经常使用的数据加cache来优化访存。mysql
若是某个位置的信息被访问,那和它相邻的信息也颇有可能被访问到。 这个也很好理解,咱们大部分状况下代码都是顺序执行,数据也是顺序访问的。linux
访问内存时,大几率会访问连续的块,而不是单一的内存地址,其实就是空间局部性在内存上的体现。目前计算机设计中,都是以块/页为单位管理调度存储,其实就是在利用空间局部性来优化性能。git
这个又被称为顺序局部性,计算机中大部分指令是顺序执行,顺序执行和非顺序执行的比例大体是5:1,即使有if这种选择分支,其实大多数状况下某个分支都是被大几率选中的,因而就有了CPU的分支预测优化。github
等距局部性是指若是某个位置被访问,那和它相邻等距离的连续地址极有可能会被访问到,它位于空间局部性和分支局部性之间。 举个例子,好比多个相同格式的数据数组,你只取其中每一个数据的一部分字段,那么他们可能在内存中地址距离是等距的,这个能够经过简单的线性预测就预测是将来访问的位置。redis
计算机领域关于局部性很是多的利用,有不少你天天都会用到,但可能并无察觉,另一些可能离你会稍微远一些,接下来咱们举几个例子来深刻了解下局部性的应用。sql
CDN的全称是Content Delivery Network,即内容分发网络(图片来自百度百科) 。CDN经常使用于大的素材下发,好比图片和视频,你在淘宝上打开一个图片,这个图片其实会就近从CDN机房拉去数据,而不是到阿里的机房拉数据,能够减小阿里机房的出口带宽占用,也能够减小用户加载素材的等待时间。
数据库
JIT全称是Just-in-time Compiler,中文名为即时编译器,是一种Java运行时的优化。Java的运行方式和C++不太同样,由于为了实现write once, run anywhere的跨平台需求,Java实现了一套字节码机制,全部的平台均可以执行一样的字节码,执行时有该平台的JVM将字节码实时翻译成该平台的机器码再执行。问题在于字节码每次执行都要翻译一次,会很耗时。
windows
一般状况下,一个方法先被解释执行(level 0),而后被C1编译(level 3),再而后被获得profile数据的C2编译(level 4)。若是编译对象很是简单,虚拟机认为经过C1编译或经过C2编译并没有区别,便会直接由C1编译且不插入profiling代码(level 1)。在C1忙碌的状况下,interpreter会触发profiling,然后方法会直接被C2编译;在C2忙碌的状况下,方法则会先由C1编译并保持较少的profiling(level 2),以获取较高的执行效率(与3级相比高30%)。
这里将少部分字节码实时编译成机器码的方式,能够提高java的运行效率。可能有人会问,为何不预先将全部的字节码编译成机器码,执行的时候不是更快更省事吗?首先机器码是和平台强相关的,linux和unix就可能有很大的不一样,况且是windows,预编译会让java失去夸平台这种优点。 其次,即时编译可让jvm拿到更多的运行时数据,根据这些数据能够对字节码作更深层次的优化,这些是C++这种预编译语言作不到的,因此有时候你写出的java代码执行效率会比C++的高。
CopyOnWrite写时复制,最先应该是源自linux系统,linux中在调用fork() 生成子进程时,子进程应该拥有和父进程同样的指令和数据,可能子进程会修改一些数据,为了不污染父进程的数据,因此要给子进程单独拷贝一份。出于效率考虑,fork时并不会直接复制,而是等到子进程的各段数据须要写入才会复制一份给子进程,故此得名 写时复制 。
在计算机的世界里,读写的分布也是有很大的局部性的,大多数状况下读远大于写, 写时复制 的方式,能够减小大量没必要要的复制,提高性能。 另外这种方式也不只仅是用在linux内核中,java的concurrent包中也提供了CopyOnWriteArrayList CopyOnWriteArraySet。像Spark中的RDD也是用CopyOnWrite来减小没必要要的RDD生成。
上面列举了那么多局部性的应用,其实还有不少不少,我只是列举出了几个我所熟知的应用,虽然上面这些例子,咱们都利用局部性获得了能效、成本上的提高。但有些时候它也会给咱们带来一些很差的体验,更多的时候它其实就是一把双刃剑,咱们如何识别局部性,利用它好的一面,避免它坏的一面?
文章开头也说过,局部性其实就是一种几率的不均等性,因此只要几率不均等就必定存在局部性,由于不少时候这种几率不均太明显了,很是好识别出来,而后咱们对大头作相应的优化就好了。但可能有些时候这种几率不均须要作很详细的计算才能发现,最后还得核对成本才能考虑是否值得去作,这种须要具体问题具体分析了。 如何识别局部性,很简单,看几率分布曲线,只要不是一条水平的直线,就必定存在局部性。
发现局部性以后对咱们而言是如何利用好这些局部性,用得好提高性能、节约资源,用很差局部性就会变成阻碍。并且不光是在计算机领域,局部性在非计算机领域也能够利用。
上面列举到的不少应用其实就是经过局部性作一些优化,虽然这些都是别人已经作好的,可是咱们也能够参考其设计思路。
恰巧最近我也在作咱们一个java服务的性能优化,利用jstack、jmap这些java自带的分析工具,找出其中最吃cpu的线程,找出最占内存的对象。我发现有个redis数据查询有问题,由于每次须要将一个大字符串解析不少个键值对,中间会产生上千个临时字符串,还须要将字符串parse成long和double。redis数据太多,不可能彻底放的内存里,可是这里的key有明显的局部性,大量的查询只会集中在头部的一些key上,我用一个LRU Cache缓存头部数据的解析结果,就能够减小大量的查redis+解析字符串的过程了。
另外也发现有个代码逻辑,每次请求会被重复执行几千次,耗费大量cpu,这种热点代码,简单几行改动减小了没必要要的调用,最终减小了近50%的CPU使用。
《高能人士的七个习惯》里提到了一种工做方式,将任务划分为重要紧急、不重要但紧急、重要但不紧急、不重要不紧急四种,这种划分方式其实就是按单位时间的重要度排序的,按单位时间的重要度越高收益越大。《The Effective Engineer》里直接用leverage(杠杆率)来衡量每一个任务的重要性。这两种方法差很少是相似的,都是优先作高收益率的事情,能够明显提高你的工做效率。
这就是工做中收益率的局部性致使的,只要少数事情有比较大的收益,才值得去作。还有一个很著名的法则__82法则__,在不少行业、不少领域均可以套用,80%的xxx来源于20%的xxx ,80%的工做收益来源于20%的工做任务,局部性给咱们的启示“永远关注最重要的20%” 。
上面咱们一直在讲如何经过局部性来提高性能,但有时候咱们须要避免局部性的产生。 好比在大数据运算时,时常会遇到数据倾斜、数据热点的问题,这就是数据分布的局部性致使的,数据倾斜每每会致使咱们的数据计算任务耗时很是长,数据热点会致使某些单节点成为整个集群的性能瓶颈,但大部分节点却很闲,这些都是咱们须要极力避免的。
通常咱们解决热点和数据切斜的方式都是提供太重新hash打乱整个数据让数据达到均匀分布,固然有些业务逻辑可能不会让你随意打乱数据,这时候就得具体问题具体分析了。感受在大数据领域,局部性极力避免,固然若是无法避免你就得经过其余方式来解决了,好比HDFS中小文件单节点读的热点,能够经过减小加副本缓解。其本质上没有避免局部性,只增长资源缓解热点了,听说微博为应对明星出轨Redis集群也是采起这种加资源的方式。