当前位置: 首页 > 科技观察

CPU通俗言情与代码级性能优化实例分析

时间:2023-03-11 22:26:13 科技观察

做任何事都必须形成自己的方法体系,这样才能游刃有余。在上一篇文章中,我们简单介绍了一个简单的例子来说明如何在代码开发中保证程序的性能。今天我们将更深入地介绍如何在代码层面提升程序性能。并将其归纳为几种情况,以便在以后的开发中加以应用。另外,本节我们主要介绍代码层面的性能优化,涉及到操作系统乃至整个分布式大系统的性能优化我们会单独介绍。程序运行在CPU上,所以在介绍性能优化之前有必要介绍一下CPU的核心结构。在上一篇文章中,我们对CPU进行了简化(如图1所示)。事实上,CPU的结构非常复杂。毕竟,CPU是由数十亿个晶体管组成的。CPU在通俗小说中的作用很好理解,就是数据处理部件。CPU就像一个大工厂,将原材料(数据)加工成半成品和成品;而内存就像一个大仓库。虽然CPU和内存都在机箱里,但是CPU访问内存里的数据不是很方便,就像工厂和大仓库的距离,有几百公里。原材料从仓库到工厂要坐火车算,一次运输可能要好几个小时。这个大工厂(CPU)里面有很多东西,最重要的有车间(CPU核心)、生产线(ALU)、物料暂存区(register)、工厂小仓库(cache)等等。为了更好的理解以上内容之间的关系,这里做一个简化的平面图。工厂加工产品所需的原材料需要从外面的大仓库运过来。由于厂外大仓库到厂区的距离比较远,需要的时间也比较长,所以一直有计划将物料从厂外大仓库分批运送到厂内小仓库。工厂的车间突然需要一些原材料,火车只能重新开跑。运输的原材料不能乱放,否则下雨、刮风会损坏。因此,原材料会统一存放在工厂内的小仓库(CPU缓存),各车间根据需要将原材料从小仓库运送到车间。车间内设有暂存区(收银台),用于存放从小仓库运来的物料。当然,暂存区除了存放原材料外,还会存放一些半成品和成品。车间有车间顺序,不能乱放,否则会出问题。暂存区是很有必要的,不然需要什么材料就去仓库拿,不像工人。一旦原材料到位,工人就可以将原材料放在生产线(ALU)上进行生产。成品将放回暂存区,然后运出。暂存区和生产线都在车间里,原材料和成品的搬运速度非常快,几乎一两分钟就可以完成。关于流水线,为了提高产品的生产速度,一个车间内通常会有多条生产线。每条生产线的大致流程是原材料运输、原材料预处理(如撕掉包装或切成小块等)、原材料加工、成品运回暂存等步骤。储藏区域。CPU也有类似的流水线。任何指令都必须被读取、解码、执行和写回(寄存器或内存)。以生产黄桃罐头的作坊为例。在这个车间里,罐头瓶、罐头瓶盖和糖水黄桃是同时生产的(这里是假设,实际工厂里不是这样)。因此,还有一条生产罐头瓶的流水线,一条生产罐头瓶盖的流水线,一条生产糖浆黄桃的流水线。通常我们安排的流程是先生产罐装瓶和瓶盖,这样生产出来的糖浆黄桃才能装瓶完成成品。但是,有时在交货集结区或工厂的小仓库中可能没有玻璃,因此无法生产罐头。不过没关系,作坊还是可以先生产糖浆黄桃,生产出来后放在暂存区,等罐头生产出来后再装瓶。上面的过程其实就是所谓的顺序紊乱。也就是说,CPU在执行指令的时候,并不是按照我们写代码的顺序执行的,而是有可能打乱顺序的。比如下面的代码,由于两行代码之间没有依赖关系,所以b=2可能先在CPU中执行,然后是a=1。inta=1;intb=2;存储金字塔结构另外一个重要的知识点是软件开发中涉及到的存储金字塔需要知道。具体来说,如图所示,寄存器、一级缓存、二级缓存、三级缓存是CPU的内部组件,然后是内存和磁盘。最后就是远程存储,比如云计算中的SAN、NAS或者对象存储或者云盘,都是远程存储。一般来说,越靠近金字塔底部,容量越大,但延迟越大,性能越差。这里有个特例,就是本地存储和远程存储。如果远程存储使用的介质与本地存储相同,那么远程存储的性能肯定更差。但目前部分分布式存储采用RDMA作为通信链路,SSD作为存储介质,因此本地机械盘的性能比远程存储要差。了解了这个结构之后,我们来总结一下。其实性能问题可以一句话概括,使用尽可能少的计算资源(比如不同的排序算法),尽可能多的使用金字塔顶端的组件来存储要访问的数据(如文件系统的缓存)。程序性能分析工具正所谓:“工欲善其事,必先利其器”。因此,为了优化性能,自然要有相应的分析工具。本文只介绍Linux操作系统,其他操作系统实在是陌生。在Linux操作系统下,使用最多的性能分析工具恐怕非top莫属。(1)top命令top命令可以实时观察进程的计算资源使用情况(CPU使用率)和整个系统的综合负载情况。如图所示,我们通过Python脚本模拟一个高负债程序,可以看到CPU利用率达到了100%。top工具可以帮助我们分析高消耗计算资源的程序的性能。此外,还有其他一些性能分析工具,如ps、vmstat、mpstat和prstat等,工具很多,限于篇幅,本文暂不介绍。性能优化方法总结有了前面的准备知识,下面进入正题。本节总结了程序代码层面的常见问题,并结合实例给出了解决方案。让我们一一分析。1.优化程序代码结构出现该问题的原因是程序代码结构不合理,导致计算资源占用过多。如果说高大上,说明算法不好。比如下面两个程序,前面的程序在for循环的条件判断中有一个strlen的调用,用来判断字符串的长度。后一段代码将strlen移到条件判断之外。如果字符串很大,两个程序的性能可能相差数百倍。这主要是因为strlen函数其实是一个循环判断,比较消耗计算资源。另一个最常见的例子与排序算法有关,例如冒泡排序的性能比快速排序差。因为两者的计算量(时间复杂度)不同,算法的性能自然也不同。2.算子的合理选择这部分也针对计算资源消耗进行了优化。在介绍这部分内容之前,我们需要先有个概念。即不同的运算(加减乘除)消耗的计算资源不同。其中加减法、位运算、移位运算是最低的,可以认为是1,然后乘法是3-4,除法大约是10-30。了解了以上内容后,我们在程序开发中应该尽量少用除法运算,因为它的性价比确实不高(太消耗计算资源了)。可能有人会想,这怎么可能?有时我们必须使用除法?让我们看一个例子。这个例子是Hashmap在JDK中的实现。Hashmap是通过哈希表来实现的,哈希表的概念这里就不啰嗦了。在查找或者存储的时候,需要根据Key值取模来定位元素的位置。通常我们能想到的方法就是模运算(计算量和除法差不多),但是Hashmap中并没有用到模运算,而是用到了位运算。这样一来,整体性能将提升十倍以上。以下是它的代码。staticintindexFor(inth,intlength){returnh&(length-1);}3.减少内存访问通过前面的准备知识我们知道内存访问比寄存器慢100倍,所以写代码的时候尽量减少内存访问。那么如何减少内存访问呢?我们还是看一个例子,比如一个简单的累加操作(这个例子比较极端)。前者通过全局变量存储累计和,而后者使用局部变量。为了深入了解两者的区别,我们需要对程序进行反汇编,然后对反汇编代码进行比较。对比网上的代码可以看出,前者每次计算都会多次访问内存(带括号的汇编语句),而后者则转化为寄存器访问。虽然我们通常认为局部变量是在函数栈(内存空间)中,但实际上,编译器在编译程序时会对代码进行优化,将局部变量优化到寄存器中。因此,我们在实际开发中尽量使用局部变量来减少对内存的访问。4.减少访问磁盘的原理和前面一样,还是存储金字塔。如果您的程序有很多磁盘访问,性能通常不会那么好。通常的做法是使用内存作为缓存。磁盘性能优化最经典的例子大概就是文件系统的pagecache了。即文件系统写入的数据不会立即写入磁盘,而是先写入缓存(内存)。读取数据时,通过预读机制将数据提前读入内存,文件系统从内存中读取数据,而不是从磁盘中读取数据。由于内存的性能是机械盘的10万多倍,文件系统的性能可以得到很大的提升(这里有场景限制,后面会详细介绍)。另一个经典案例与文件系统有关,即Linux虚拟文件系统(VFS)。我们知道,文件系统中的每个文件对应一个inode,inode也存储在磁盘上。如果我们要打开一个文件,首先需要从磁盘中找到inode,然后将其读入内存,然后进行后续的读写操作。在VFS中,当文件打开时,VFS会将inode放入内存中的哈希表中,文件关闭时不会释放。这样,当应用程序再次打开文件时,可以直接从内存中找到inode,而不需要重新读取磁盘。以上都是特例,大家要好好掌握,希望对大家的软件设计有所帮助。最后,性能优化的本质还是那句话,尽可能少地使用计算资源,尽可能多地使用金字塔顶端的组件来存储要访问的数据。