软件设计杂谈——性能优化的十种手段(上篇)

2023-04-07  本文已影响0人  生饼

本文转自 code2life,查看原文请点击 这里

目录

引言:取与舍

软件设计开发某种意义上是“取”与“舍”的艺术。关于性能方面,就像建筑设计成抗震9度需要额外的成本一样,高性能软件系统也意味着更高的实现成本,有时候与其他质量属性甚至会冲突,比如安全性、可扩展性、可观测性等等。大部分时候我们需要的是:在业务遇到瓶颈之前,利用常见的技术手段将系统优化到预期水平。那么,性能优化有哪些技术方向和手段呢?

性能优化通常是“时间”与“空间”的互换与取舍。本篇分两个部分,在上篇,讲解六种通用的“时间”与“空间”互换取舍的手段:

(注:所有配图来自动漫《火影忍者》,部分图片添加了文字方便理解,仅作技术交流用途)

索引术

索引的原理是拿额外的存储空间换取查询时间,增加了写入数据的开销,但是读取数据的时间复杂度一般从O(n)降低到O(logn)甚至O(1)。索引不仅在数据库中广泛使用,前后端的开发中也在不知不觉运用。

在数据集比较大时,不用索引就像从一本没有目录而且内容乱序的新华字典查一个字,得一页一页全翻一遍才能找到;用索引之后,就像用拼音先在目录中先找到要查到字在哪一页,直接翻过去就行了。书籍的目录是典型的树状结构,那么软件世界常见的索引有哪些数据结构,分别在什么场景使用呢?

数据库主键之争:自增长 vs UUID。主键是很多数据库非常重要的索引,尤其是MySQL这样的RDBMS会经常面临这个难题:是用自增长的ID还是随机的UUID做主键?

自增长ID的性能最高,但不好做分库分表后的全局唯一ID,自增长的规律可能泄露业务信息;而UUID不具有可读性且太占存储空间。争执的结果就是找一个兼具二者的优点的折衷方案:用雪花算法生成分布式环境全局唯一的ID作为业务表主键,性能尚可、不那么占存储、又能保证全局单调递增,但引入了额外的复杂性,再次体现了取舍之道。

再回到数据库中的索引,建索引要注意哪些点呢?

数据库之外,在代码中也能应用索引的思维,比如对于集合中大量数据的查找,使用Set、Map、Tree这样的数据结构,其实也是在用哈希索引或树状索引,比直接遍历列表或数组查找的性能高很多。

缓存术

缓存优化性能的原理和索引一样,是拿额外的存储空间换取查询时间。缓存无处不在,设想一下我们在浏览器打开这篇文章,会有多少层缓存呢?

缓存是“银弹”吗?

不,Phil Karlton 曾说过:

计算机科学中只有两件困难的事情:缓存失效和命名规范。
There are only two hard things in Computer Science: cache invalidation and naming things.

缓存的使用除了带来额外的复杂度以外,还面临如何处理缓存失效的问题。

压缩术

说完了两个“空间换时间”的,我们再看一个“时间换空间”的办法——压缩。压缩的原理消耗计算的时间,换一种更紧凑的编码方式来表示数据

为什么要拿时间换空间?时间不是最宝贵的资源吗?

举一个视频网站的例子,如果不对视频做任何压缩编码,因为带宽有限,巨大的数据量在网络传输的耗时会比编码压缩的耗时多得多。对数据的压缩虽然消耗了时间来换取更小的空间存储,但更小的存储空间会在另一个维度带来更大的时间收益

这个例子本质上是:“操作系统内核与网络设备处理负担 vs 压缩解压的CPU/GPU负担”的权衡和取舍。

我们在代码中通常用的是无损压缩,比如下面这些场景:

信息论告诉我们,无损压缩的极限是信息熵。进一步减小体积只能以损失部分信息为代价,也就是有损压缩

image.png

那么,有损压缩有哪些应用呢?

除了有损/无损压缩,但还有一个办法,就是压缩的极端——从根本上减少数据或彻底删除

能减少的就减少

能删除的就删除

No code is the best way to write secure and reliable applications. Write nothing; deploy nowhere. —— Kelsey Hightower

<main class="container" style="display: block; margin: 2em 10px; color: rgb(52, 73, 94); font-family: sourcesanspro, "Helvetica Neue", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">

<article class="post-block" style="display: block;">

预取术

预取通常搭配缓存一起用,其原理是在缓存空间换时间基础上更进一步,再加上一次“时间换时间”,也就是:用事先预取的耗时,换取第一次加载的时间。当可以猜测出以后的某个时间很有可能会用到某种数据时,把数据预先取到需要用的地方,能大幅度提升用户体验或服务端响应速度。

是否用预取模式就像自助餐餐厅与厨师现做的区别,在自助餐餐厅可以直接拿做好的菜品,一般餐厅需要坐下来等菜品现做。那么,预取在哪些实际场景会用呢?

天上不会掉馅饼,预取也是有副作用的。正如烤箱预热需要消耗时间和额外的电费,在软件代码中做预取/预热的副作用通常是启动慢一些、占用一些闲时的计算资源、可能取到的不一定是后面需要的

削峰填谷术

削峰填谷的原理也是“时间换时间”,谷时换峰时。削峰填谷与预取是反过来的:预取是事先花时间做,削峰填谷是事后花时间做。就像三峡大坝可以抗住短期巨量洪水,事后雨停再慢慢开闸防水。软件世界的“削峰填谷”是类似的,只是不是用三峡大坝实现,而是用消息队列、异步化等方式。

常见的有这几类问题,我们分别来看每种对应的解决方案:

批量处理术

批量处理同样可以看成“时间换时间”,其原理是减少了重复的事情,是一种对执行流程的压缩。以个别批量操作更长的耗时为代价,在整体上换取了更多的时间

批量处理的应用也非常广泛,我们还是从前端开始讲:

批量处理如此好用,那么问题来了,每一批放多大最合适呢

这个问题其实没有定论,有一些个人经验可以分享。

总之,多大一批可以确保单批响应时间不太长的同时让整体性能最高,是需要在实际情况下做基准测试的,不能一概而论。而批量处理的副作用在于:处理逻辑会更加复杂,尤其是一些涉及事务、并发的问题;需要用数组或队列用来存放缓冲一批数据,消耗了额外的存储空间。

小结

上半部分先聊到这里,大都是“时间”与“空间”的取舍之术,这些思路在很多地方甚至是非软件领域都是普适的。下半部分我们再聊一些不完全普适、稍微进阶一点的性能优化的技术路线。

上一篇 下一篇

猜你喜欢

热点阅读