2

如何写出高性能代码(四)优化数据访问

 1 year ago
source link: https://zxs.io/article/1868
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
在这里插入图片描述

  同一份逻辑,不同人的实现的代码性能会出现数量级的差异; 同一份代码,你可能微调几个字符或者某行代码的顺序,就会有数倍的性能提升;同一份代码,也可能在不同处理器上运行也会有几倍的性能差异;十倍程序员不是只存在于传说中,可能在我们的周围也比比皆是。十倍体现在程序员的方法面面,而代码性能却是其中最直观的一面。

  本文是《如何写出高性能代码》系列的第四篇,本文将告诉你数据访问会怎么样影响到程序的性能,以及如何通过变更数据访问的方式提升程序的性能。

数据访问速度为什么会影响到程序的性能?

  程序的运行的每一个可以简化为这样一个三步模型:第一步,读数据(当然也有部分数据是别的地方法发过来的);第二步,对数据做处理;第三步,将处理完的结果写入存储器。这里我将这三步骤简称为 读算写。 实际上真实的CPU指令执行过程会稍微复杂有些,但实际上也是这三个步骤。 而一个复杂的程序包含无数个CPU指令,如果读取或者写入数据太慢,必然会影响到程序的性能。

在这里插入图片描述

  为了能更直观一点,我这里将程序执行的流程比作是大厨做菜,大厨的工作流程就是取原始食材,然后对食材进行加工(煎烤烹炸煮),最后出锅上菜。影响大厨出菜速度的因素除了加工过程之前,获取食材的耗时也会影响到大厨出菜速度。有些食材就在手边,可以很快获取到,但有些食材可能在冷库、甚至在菜市场,获取就很不方便了。

  CPU犹如大厨,而数据就是CPU的食材,寄存器里的数据就是CPU手边的食材,内存的数据就是在冷库的食材,固态硬盘(SSD)上的数据是还在菜市场的食材,机械硬盘(HDD)上的数据犹如还在地里生长的菜…… 如果CPU在运行程序时,如果拿不到所需要的数据,它也只能等在那儿浪费时间了。

数据访问速度对程序性能有多大影响?

  不同存储器数据读取和写入的时延相差极大,鉴于大多数场景下,我们都是读取数据,我们就只拿数据读取为例,最快的寄存器和最慢的机械磁盘,随机读写的时延相差百万倍。可能你没有直观概念,我们还是拿厨师做个类比。

   假设厨师要做一道西红柿炒鸡蛋,如果食材都有人备好的话,只需要十来秒食材就能下锅炒制。 我们把这个时间比作是CPU从寄存器里取到数据的时间。然而如果是CPU从磁盘获取数据的话,所耗费的时间相当于厨师自己种出西红柿或者养小鸡下蛋了(3-4个月)。由此可见,从错误的存储设备上获取数据,会极大影响程序的运行速度。

  再说一个我们之前在生产环境遇到的实际案例,我们在生产环境也出过故障。原因是这样的,我们有个服务容器化改造的时候,和上游服务没有部署在同一个机房,跨机房虽然只会增加1ms的时延,但他们服务代码写的有问题,有个接口批量串行调另外一个服务,串行累加导致接口时延增加上百ms。 本来没有性能问题的服务,就因为迁移了机房,导致性能出现了问题……

各存储器性能差异

  实际上在编码的时候,遇到的存储设备多种多样,寄存器、内存、磁盘、网络存储……,每种设备都有自己的特点。只有认识到各种存储器之间的差异,我们才能在正确的场景下使用合适的存储器。以下表格就是各类常见存储设备的随机读时延参考数据……

在这里插入图片描述

备注:以上数据在不同硬件设备会有出入,这里只是为了展示其差异性,不代表准确值,准确信息请参考硬件手册。

  虽然日常我们觉得内存的读取速度已经很非常快了,日常写代码的时候遇到啥数据获取比较慢,加个内存缓存速度简直就起飞了。但内存的访问速度相对于CPU运行速度来说还是太慢,读取一次内存的时间,都够CPU执行几百条指令了,所以现代CPU都对内存加了缓存。

如何减小数据访问时延对性能的影响?

  减少数据访问时延对性能的影响也很简单,那就是把数据尽可能放到最快的存储介质上。然而,存取速度、容量、价格三者之间有着不可调和的矛盾,简单来说就是 速度越快容量越小但价格越贵,反之容量越大速度越慢而价格越便宜

在这里插入图片描述

  世界总是那么巧秒,仿佛一切早被安排好,我们并不需要把所有的数据都放在最快的存储介质上。 还记得我们在第二篇(巧用数据特性)[https://blog.csdn.net/xindoo/article/details/123941141] 提到的数据局部性吗! 局部性分两种,空间局部性和时间局部性。

  • 时间局部性: 如果一份某个时刻数据被访问过,那不久之后这份数据会被再次访问到。
  • 空间局部性: 如果某个存储元被访问过,大概率那不久之后,其附近的存储单元也会被访问。

  总结下这两点就是,程序大部分时间只会集中访问很小的一部分数据。 这意味着我们可以用较小的存储空间覆盖到大部分被访问的数据。 说直接点就是,我们可以加缓存。 实际上,不管是计算机硬件、数据库、还是业务系统,到处都充斥着缓存。甚至你写下的每一行代码,在机器上运行时都用到了缓存,不知道大家有没有关注过CPU,CPU有个参数,就是缓存大小,我们以intel酷睿i7-12650HX 为例,它就有24MB的三级缓存,这个缓存就是CPU到内存之间的缓存。 只不过现代计算机将底层的细节屏蔽掉了而已,我们日常不太可能主要的到。

  在我们自己写代码的时候,也可以加缓存来提升程序性能。举个最近的我们在系统中遇到的例子,我们最新在做数据权限相关的功能,不同的员工在我们系统中有不同的权限,所以他们看到的数据也应该是不同的。我们的实现方式是每个用户请求系统的时候,首先获取到该用户所有的权限列表,然后把所有在权限列表中的数据展示出来。

  因为每个人的权限列表比较大,所以权限接口的性能不怎么样,每次请求耗时也比较长。所以,我们直接给这个接口的数据加了缓存,优先从缓存里取,取不到再调接口,极大提升了程序性能。当然因为权限数据也不会经常变动,所以也不用太考虑数据滞后导致的后果。另外,我们缓存数据只加了几分钟,因为一个用户单次使用我们系统时长也就持续几分钟,过几分钟后数据过期缓存空间也会自动释放,达到节省空间的目的。

  我在上大学那会,笔记本电脑还是标配机械硬盘的年代,那时候电脑永久了会很卡,后来了解到换装SSD会提升电脑性能,那个时候SSD还挺贵的,普通笔本都不会标配SSD后来我攒半个月的生活费给自己笔记本替换了一块120g的SSD,电脑的运行速度就有明显的提升,本质上还是因为SSD的随机访问时延比机械硬盘快上百倍的原因。 之前某大厂号称将mysql性能提升了上百倍,其实也是基于SSD做的很多查询优化。

缓存不是银弹

银弹(英文:Silver Bullet),指由纯银质或镀银的子弹。在欧洲民间传说及19世纪以来哥特小说风潮的影响下,银色子弹往往被描绘成具有驱魔功效的武器,是针对狼人、吸血鬼等超自然怪物的特效武器。后来也被比喻为具有极端有效性的解决方法,作为杀手锏、最强杀招、王牌等的代称。

在这里插入图片描述

  这里特别提醒下,缓存不是万能的,缓存其实是有副作用的,那就是数据的有效性很难得到保证。缓存其实里面放的是旧数据,当前时刻数据是不是还是这样的?不确定,也许数据早就变了,所以使用缓存时必须要关注缓存数据有效性问题。如果缓存时间过久,数据失效的可能性能,数据不一致导致的风险也就越大。 如果缓存时间过短,因为经常需要获取原始数据,缓存存在意义也就越小。所以在使用缓存必须要做出数据不一致和性能之间的权衡(trade-off),你需要正确评估数据的时效性,对缓存设置合理的过期策略。

  上文说到其实我们写下的每一行代码都用到了缓存,现在大家已经都知道这个缓存其实就是CPU的Cache。CPU的Cache也是有明显的副作用的,我们在写多线程代码的时候也不得不关注到,那就是多核CPU之间数据一致性的问题。因为CPU Cache的存在,我们写多线程代码时不得不考虑数据同步的问题,导致多线程的代码很难编写,出了问题也很难排查。

  有个面试八股文题目其实就很容易说明这个问题——多线程计数器,多线程去操作计数器,累加统计数据,如何保证数据统计的准确性。如果只是简单使用cnt++实现,这里就会遇到多核CPU缓存导致的数据不一致性,具体原理这里不再解释,反正结果就是统计出来的数据会比真是数据少。 正确的做法就是,你必须在累加的过程中加多线程同步的机制,保证同一时刻只可能有一个线程在操作,操作完之后也能保证数据能写回内存,在java中必须使用锁或者原子类实现。而这对于编程新手而言又是一道门槛。


  数据访问是任何程序不可或缺的一部分,甚至对大多数程序而言时间都耗费在了数据访问的过程上,所以只要优化了这部分的耗时,程序的性能必然能得到提升。

  本文全部内容就到这了,下一篇,我们将继续探讨下性能优化到极致该怎么做,敬请期待!!另外,有兴趣也可以查阅下之前的几篇文章。

如何写出高性能代码系列文章


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK