上一篇,我们总结了六种普适的性能优化方法,包括 索引、压缩、缓存、预取、削峰填谷、批量处理,简单讲解了每种技术手段的原理和实际应用。在开启最后一篇前,我们先需要搞清楚:在程序运行期间,时间和空间都耗在哪里了?
人眨一次眼大约100毫秒,而现代1核CPU在一眨眼的功夫就可以执行数亿条指令。
现代的CPU已经非常厉害了,频率已经达到了GHz级别,也就是每秒数十亿个指令周期。
即使一些CPU指令需要多个时钟周期,但由于有流水线机制的存在,平均下来大约每个时钟周期能执行1条指令,比如一个3GHz频率的CPU核心,每秒大概可以执行20亿到40亿左右的指令数量。
程序运行还需要RAM,也可能用到持久化存储,网络等等。随着新的技术和工艺的出现,这些硬件也越来越厉害,比如CPU高速缓存的提升、NVMe固态硬盘相对SATA盘读写速率和延迟的飞跃等等。这些硬件具体有多强呢?
有一个非常棒的网站“Latency Numbers Every Programmer Should Know”,可以直观地查看从1990年到现在,高速缓存、内存、硬盘、网络时间开销的具体数值。
https://colin-scott.github.io/personal_website/research/interactive_latency.html
下图是2020年的截图,的确是“每个开发者应该知道的数字”。
这里有几个非常关键的数据:
看到不同硬件之间数量级的差距,就很容易理解性能优化的一些技术手段了。
比如一次网络传输的时间,是主存访问的5000倍,明白这点就不难理解写for循环发HTTP请求,为什么会被扣工资了。
放大到我们容易感知的时间范围,来理解5000倍的差距:如果一次主存访问是1天的话,一趟局域网数据传输就要13.7年。
如果要传输更多网络数据,每两个网络帧之间还有固定的间隔(Interpacket Gap),在间隔期间传输Idle信号,数据链路层以此来区分两个数据包,具体数值在链接Wiki中有,这里截取几个我们熟悉的网络来感受一下:
不过,单纯看硬件的上限意义不大,从代码到机器指令中间有许多层抽象,仅仅是在TCP连接上发一个字节的数据包,从操作系统内核到网线,涉及到的基础设施级别的软硬件不计其数。到了应用层,单次操作耗时虽然没有非常精确的数字,但经验上的范围也值得参考:
在计算机历史上,非易失存储技术的发展速度超过了摩尔定律。除了嵌入式设备、数据库系统等等,现在大部分场景已经不太需要优化持久化存储的空间占用了,这里主要讲的是另一个相对稀缺的存储形式 —— RAM,或者说主存/内存。
以JVM为例。
堆里面有很多我们创建的对象(Object)。
如果在32G以上内存的机器上,禁用了对象指针压缩,对象指针会变成8字节,包括Header中的Klass指针,这也就不难理解为什么堆内存超过32G,JVM的性能直线下降了。
举个例子,一个有8个int类型成员的对象,需要占用48个字节(12+32+4),如果有十万个这样的Object,就需要占用4.58MB的内存了。这个数字似乎看起来不大,而实际上一个Java服务的堆内存里面,各种各样的对象占用的内存通常比这个数字多得多,大部分内存耗在char[]这类数组或集合型数据类型上。
堆内存外面,又是另一个世界了。
从操作系统进程的角度去看,也有不少耗内存的大户,不管什么Runtime都逃不开这些空间开销:每个线程需要分配MB级别的线程栈,运行的程序和数据会缓存下来,用到的输入输出设备需要缓冲区……
代码“写出来”的内存占用,仅仅是冰山之上的部分,真正的内存占用比“写出来”的要更多,到处都存在空间利用率的问题。
比如,即使我们在Java代码中只是写了 response.getWriter().print("OK"),给浏览器返回2字节,网络协议栈的层层封装,协议头部不断增加的额外数据,让最终返回给浏览器的字节数远超原始的2字节,像IP协议的报头部就至少有20个字节,而数据链路层的一个以太网帧头部至少有18字节。
如果传输的数据过大,各层协议还有最大传输单元MTU的限制,IPv4一个报文最大只能有64K比特,超过此值需要分拆发送并在接收端组合,更多额外的报头导致空间利用率降低(IPv6则提供了Jumbogram机制,最大单包4G比特,“浪费”就减少了)。
这部分的“浪费”有多大呢?下面的链接有个表格,传输1460个字节的载荷,经过有线到无线网络的转换,至少再添120个字节,空间利用率<92.4%。
https://en.wikipedia.org/wiki/Jumbo_frame
这种现象非常普遍,使用抽象层级越高的技术平台,平台提供高级能力的同时,其底层实现的“信息密度”通常越低。像Java的Object Header就是使用JVM的代价,而更进一步使用动态类型语言,要为灵活性付出空间的代价则更大。哈希表的自动扩容,强大的反射能力等等,背后也付出了空间的代价。
再比如,二进制数据交换协议通常比纯文本协议更加节约空间。但多数厂家我们仍然用JSON、XML等纯文本协议,用信息的冗余来换取可读性。即便是二进制的数据交互格式,也会存在信息冗余,只能通过更好的协议和压缩算法,尽量去逼近压缩的极限 —— 信息熵。
理解了时间和空间的消耗在哪后,还不能完全解释软件为何倾向于耗尽硬件资源。有一条定律可以解释,正是它锤爆了摩尔定律。
它就是安迪-比尔定律。
“安迪给什么,比尔拿走什么”。
安迪指的是Intel前CEO安迪·葛洛夫,比尔指的是比尔·盖茨。这句话的意思就是:软件发展比硬件还快,总能吃得下硬件。
20年前,在最强的计算机也不见得可以玩赛车游戏;10年前,个人电脑已经可以玩画质还可以的3D赛车游戏了;现在,自动驾驶+5G云驾驶已经快成为现实。在这背后,是无数的硬件技术飞跃,以及吃掉了这些硬件的各类软件。这也是我们每隔两三年都要换手机的原因:不是机器老化变卡了,是嗜血的软件在作怪。
因此,即使现代的硬件水平已经强悍到如此境地,性能优化仍然是有必要的。软件日益复杂,抽象层级越来越高,就越需要底层基础设施被充分优化。对于大部分开发者而言,高层代码逐步走向低代码化、可视化,“一行代码”能产生的影响也越来越大,写出低效代码则会吃掉更多的硬件资源。
原本打算上篇写6种性能优化方法,下篇写4种,但写下半部分的时候发现涉及到技术细节,必要先了解这些数字有具体印象之后,理解后面4种方法才更深入,于是夹了一个“中篇”来承上启下吧。
公司名称: 亚游-亚游娱乐-注册登录站
手 机: 13800000000
电 话: 400-123-4567
邮 箱: admin@youweb.com
地 址: 广东省广州市天河区88号