做任何事情都要形成自己的方法体系,这样做事情才能游刃有余。在上一篇文章中,我们简要介绍了一个简单的例子来说明如何在代码开发中保证程序的性能。今天,我们将更深入地介绍如何在代码级别提高程序的性能。并且我们总结成几种情况,以便在以后的开发中应用。另外,本节主要介绍代码级的性能优化,涉及操作系统甚至整个分布式大型系统的性能优化我们单独介绍。
程序运行在CPU上,所以在介绍性能优化之前,有必要先介绍一下CPU的内核结构。在上一篇文章中,我们简化了CPU。其实CPU的结构很复杂。毕竟,CPU是由数十亿个晶体管组成的。
CPU的流行浪漫
CPU的功能很好理解,它是一个数据处理部件。CPU就像一个大工厂,把原材料加工成半成品和成品;而记忆就像一个大仓库。虽然CPU和内存都在机箱里,但是CPU访问内存里的数据不是很方便,就像工厂和大仓库的距离有几百公里一样。原材料从仓库到工厂要经过火车计算,一次运送材料可能需要几个小时。
这个大工厂的东西很多,最重要的是车间、生产线、物料暂存区、工厂的小仓库等。为了更好的理解以上内容之间的关系,这里做一个简化的方案。
工厂加工产品所需的原材料需要从外面的大仓库运过来。因为厂外的大仓库到工厂的距离比较远,需要的时间也比较长,所以一直都有把材料从厂外的大仓库分批运到厂内的小仓库的计划。工厂的车间突然需要一些原材料,我们只好让火车重新运行。
运输的原料不能放错地方,否则下雨刮风都会损坏。所以原料会统一放在工厂的小仓库里,各车间根据需要从小仓库把原料运到车间。
车间里有一个临时存储区,用来存放从小仓库运过来的材料。当然,除了原材料,临时存储区还会有一些半成品和成品。车间里是有秩序的,不能乱放,否则会出问题。临时存储区是非常必要的,或者如果你需要一些材料,去仓库拿。不像工人。
有了原材料,工人就可以放到生产线上进行生产。成品将放回临时储存区,然后运出。临时存储区和生产线都在车间,原料和成品的搬运速度非常快,几乎一两分钟就能完成。
关于装配线
为了提高产品的生产速度,一个车间通常有多条生产线。每条生产线的大致流程是原料运输、原料预处理、原料加工、成品运回暂存区。CPU也有类似的流水线。任何指令都必须被读取、解码、执行和写回。
以一个生产黄桃罐头的作坊为例。这个车间要同时生产罐装瓶、罐装瓶盖和糖水黄桃。所以有罐装瓶,罐装瓶盖,糖水黄桃的生产线。通常我们安排的流程是先生产罐装瓶和瓶盖,这样生产出来的糖水黄桃才能装瓶完成成品。但是有时候可能临时存放区或者工厂的小仓库里没有玻璃,所以无法生产易拉罐。然而,这并不重要。作坊还是可以先生产糖水桃的。生产完成后,先放在暂存区,易拉罐生产完成后何时装瓶。
以上过程其实就是所谓的指令紊乱。也就是说,CPU在执行指令的时候,并不是按照我们写的顺序来执行代码,而可能是乱序。比如下面这段代码,因为两行代码之间没有依赖关系,可能在CPU中先执行b=2,再执行a=1。
int a = 1;int b = 2;
存储金字塔结构的另一个重要知识点是需要了解软件开发涉及的存储金字塔。如图所示,寄存器、L1缓存、L2缓存和L3缓存是CPU的内部组件,其次是内存和磁盘。最后,远程存储,比如SAN、NAS或者云计算中的对象存储或者云盘,都属于远程存储。
一般来说,越往金字塔底层走,容量越大,但延迟越大,性能越差。这里有一个特例,就是本地存储和远程存储。如果远程存储使用的介质与本地存储使用的介质相同,那么远程存储的性能肯定会更差。但是,目前一些分布式存储系统采用RDMA作为通信链路,SSD作为存储介质,因此本地机械磁盘的性能要比远程存储差。
了解了这个结构之后,我们来总结一下。其实性能问题可以归结为一句话,用尽可能少的计算资源,用尽可能多的金字塔顶端的组件来存储要访问的数据。
程序性能分析工具
俗话说“欲善其事,必先利其器。”所以为了优化性能,自然要有相应的工具进行分析。本文只介绍Linux操作系统,其他操作系统真的很陌生。在Linux操作系统下,最常用的性能分析工具恐怕就是top了。
最高命令
top command可以实时观察进程的计算资源使用情况和整个系统的综合负载。如图,当我们通过Python脚本模拟一个高负债程序时,可以看到CPU利用率已经达到了100%。
Top tool可以帮助我们分析消耗高计算资源的程序的性能。还有其他性能分析工具,如ps、vmstat、mpstat和prstat。工具有很多,限于篇幅,本文暂不介绍。
性能优化方法综述
有了前面的备考知识,接下来就进入正题。本节总结了程序代码层面的常见问题,并举例给出了解决方案。让我们逐一分析。
优化程序代码结构
造成这个问题的原因在于程序代码结构不合理,导致计算资源的过度使用。如果很高,那就是算法不行。比如下面两个程序,前面的程序在For循环的条件判断中有一个strlen调用,用来判断字符串的长度。然后一段代码将strlen移到条件判断之外。
如果字符串很大,两个程序的性能可能相差上百倍。这主要是因为strlen函数实际上是一个循环判断,消耗了大量的计算资源。
另一个最常见的例子是关于排序算法,比如冒泡排序,比快速排序更差。由于计算量的不同,算法的性能自然也不同。
合理选择经营者
这部分也是计算资源消耗的优化。在介绍这部分之前,我们心里要有个概念。即不同的运算消耗的计算资源不同,其中加、减、位运算、移位运算最低,可以认为是1,那么乘法是3-4,除法法则大概是10-30。
了解了以上内容,那么在程序开发中就要尽量少用除法,因为它的性价比真的不高。有些人可能会想,这怎么可能?有时候你不得不用除法。我该怎么办?让我们看一个例子。这个例子是Hashmap在JDK的实现。
Hashmap是通过哈希表实现的,所以哈希表的概念这里就不赘述了。在搜索或存储时,需要根据键值取模块,定位元素的位置。通常我们可以想到模运算符,但是在Hashmap中,我们不用模运算符,而是用位运算。这样整个性能会提升十倍以上。这是它的代码。
{return h 的静态整数索引;}
减少对内存的访问
从之前的备考知识中我们知道,内存访问比寄存器慢100倍,所以写代码的时候尽量减少对内存的访问。那么如何减少对内存的访问呢?我们还是看一个例子,比如一个简单的加法运算。前者通过全局变量存储累积和,后者通过局部变量存储累积和。
为了理解两者的区别,我们需要对程序进行反汇编,然后对反汇编后的代码进行对比。对比在线代码可以看出,前者每次计算对内存的访问次数很多,而后者则转化为寄存器访问。
虽然我们通常认为局部变量在函数栈中,但实际上编译器在编译程序时会对代码进行优化,将局部变量优化到寄存器中。所以我们在实际开发中尽量使用局部变量,减少对内存的访问。
减少对磁盘的访问。
原因和上一个一样,还是存储金字塔。如果你的程序有很多对磁盘的访问,性能通常不会那么好。通常的方法是使用内存作为缓存。磁盘性能优化的经典例子大概就是文件系统的页面缓存了。也就是说,文件系统写入的数据不是立即写入磁盘,而是先写入缓存。读取数据时,通过预读机制将数据提前读入内存,文件系统从内存而不是磁盘读取数据。因为内存的性能是机械磁盘的10万倍以上,所以文件系统的性能大大提高。
另一个经典案例与文件系统有关。这是Linux的虚拟文件系统。我们知道文件系统中的每个文件都对应一个inode,inode也存储在磁盘上。如果要打开一个文件,首先需要从磁盘中找到inode,然后读入内存,然后才能进行后续的读写操作。
在VFS,当文件打开时,VFS会将inode放入内存中的哈希表,当文件关闭时,它不会被释放。这样,当应用程序再次打开文件时,它可以直接从内存中找到inode,而不必重新读取磁盘。
这些都是特例,要互相借鉴,希望对你的软件设计有所帮助。最后,性能优化的本质
还是那句话,用尽可能少的计算资源,用尽可能多的金字塔顶端的组件来存储要访问的数据。
免责声明:本平台仅供信息发布交流之途,请谨慎判断信息真伪。如遇虚假诈骗信息,请立即举报
举报