本文转载自微信公众号“砖码杂工”,作者我不想种田。转载本文请联系码砖手公众号。#1.思维导图#2.什么是性能优化?性能优化是指在不影响系统运行正确性的前提下,让它运行得更快,在更短的时间内完成特定的功能,或者拥有更强大的系统。服务能力。##Concern不同的程序有不同的性能关注点。例如,科学计算注重计算速度。比如游戏引擎注重渲染效率,而服务程序追求吞吐量。服务器通常是可水平扩展的分布式系统。系统处理能力取决于单机负载能力和水平扩展能力。因此,提高单机性能和提高水平扩展能力是两个主要方向。理论上,系统的水平方向可以无限扩展。但横向扩展后,往往面临通信成本高(甚至瓶颈)、单机处理能力下降等问题。##衡量单机性能的指标有很多,比如:QPS(QueryPerSecond)、TPS、OPS、IOPS、最大连接数、并发数等评价吞吐量的指标。CPU为了提高吞吐量,将指令的执行分成多个阶段,使用指令流水线。同样,软件系统为了提高处理能力,往往会引入批处理(数据包堆积)等,但与CPU流水线带来的延迟增加是一样的。伴随着系统负载的增加,延迟(Latency)也会增加。可见系统吞吐量和延迟是两个相互冲突的目标。显然,过高的延迟是不可接受的,因此服务器性能优化的目标往往变成:在可容忍的延迟(Latency)下追求最大的吞吐量(Throughput)。延迟(也称为响应时间:RT)不是固定的,通常在一个范围内波动。我们可以用平均延迟来评价系统性能,但是有时候,平均延迟是不够的,这个很好理解,比如80%的请求都在10毫秒内响应,但是20%的请求延迟超过2毫秒秒,而且这20%的高延迟可能会引起投诉,这也是不能接受的。一个改进措施是使用TP90和TP99等指标。不是取平均值,而是需要保证排序后90%和99%的请求满足时延要求。通常,执行效率(CPU)是我们关注的重点,但有时,我们还需要关注内存使用、网络带宽、磁盘IO等,影响性能的因素很多,是一个复杂的问题。#3.基于基础知识编写和运行正确程序的能力并不一定会导致性能优化。扎实的系统知识需要丰富的实践经验。只有这样,你才能具备逐案分析和解决问题的能力。因此,与其直接给出结论,我更愿意花更多篇幅介绍一些基础知识。我坚持底层基础是理解和掌握性能优化技巧的前提,花一些时间掌握这些根本技术是值得的。##CPU架构你需要了解CPU架构,了解计算单元、内存单元、控制单元是如何各司其职,相互配合完成工作的。你需要了解CPU是如何读取数据的,CPU是如何执行任务的。您需要了解数据总线、地址总线和控制总线的区别和作用。您需要了解指令周期:获取、翻译、执行、写回。你需要了解CPU流水线、超标量流水线、乱序执行。您需要了解多CPU、多核、逻辑核心、超线程、多线程和协程的概念。##存储金字塔CPU速度和内存访问速度相差200倍。缓存桥弥合了这一差距。您需要了解存储金字塔,这种分层思维是基于局部性原则的。硬件和软件系统的设计和性能有很大的影响。局部性又分为空间局部性和时间局部性。###缓存现代计算机系统一般都有L1-L2-L3三级缓存。每个CPU核心都有独立的L1和L2缓存,所以L1和L2是片上缓存;L3由多个CPU核心共享,是片外缓存。一级缓存分为i-cache(指令缓存)和d-cache(数据缓存)。一级缓存通常只有32K/64KB,速度高达4个周期。二级缓存可达256KB,速度约为8个周期。L3高达30MB,速度约为32个周期。内存高达数G,内存访问延迟约200个周期。因此CPU->register->L1->L2->L3->memory->disk构成了一个存储层级。越靠近CPU,存储容量越小,速度越快,单位成本越高。离CPU越远,存储容量越高。更大、更慢、更低的单位成本。###虚拟内存(VM)进程和虚拟地址空间是操作系统的两个核心抽象。系统中的所有进程共享CPU和主存资源。虚拟存储是主存的抽象。它为每个进程提供了一个大的、一致的、私有的地址空间。我们在调试gdb时,打印出来的变量地址就是虚拟地址。操作系统+CPU硬件(MMU)紧密配合,完成虚拟地址到物理地址的转换(映射)。这个过程总是安静和自动的,没有应用程序员的任何干预。每个进程都有一个独立的页表(PageTable)。页表是页表条目(PTE)的数组。表的内容由操作系统管理。虚拟地址空间中的每一页(4M或8M)都通过查找页表找到物理地址。页表通常是分层的。多级页表减少了页表的存储需求。PageFault会导致分页(Swapping或Paging)。这个惩罚非常重,所以,我们需要改进程序的行为,使其具有更好的局部性。如果内存访问的地址在一段时间内过于发散,就会引起抖动(Thrashing),严重影响程序的性能。为了加快地址转换的速度,在MMU中加入了一个关于PTE的小缓存,称为translationlook-asidebuffer(TLB)。地址转换单元在进行地址转换时,首先会查询TLB。L1-2-3)。##汇编基础理解汇编,理解几种寻址方式,理解数据操作,分支,转移,控制跳转指令。了解C语言的ifelse、while/dowhile/for、switchcase、函数调用是如何翻译成汇编代码的。了解ebp+esp在函数调用期间如何注册构建和撤消堆栈帧。了解函数参数和返回值是如何传递的。##异常和系统调用异常会导致控制流突然改变。异常的控制流发生在计算机系统的各个层次。异常可分为四类:中断(interrupt):中断异步发生,来自处理器的外部IO设备信号,中断处理程序分为上下两部分。陷阱:陷阱是故意的异常,它是执行指令的结果。系统调用是通过陷阱实现的。陷阱提供了一个接口“系统调用”,就像用户程序和内核之间的过程调用。故障:故障是由错误条件引起的。它可以由故障处理程序修复。当故障发生时,处理器将控制转移到故障处理程序。PageFault是一个典型的错误。实例终止(中止):Termination是不可恢复的致命错误(通常是硬件错误)导致程序执行终止的结果。##内核态和用户态你需要了解操作系统的一些概念,比如内核态和用户态。该应用程序运行我们在用户模式下编写的逻辑。一旦系统调用被调用,就会通过特定的中断进入内核态。系统调用号标识的函数不同于普通的函数调用。进入内核态和从内核态返回时,需要进行上下文切换,需要保存和恢复环境变量。会带来额外的消耗,我们写的程序应该避免频繁的进行上下文交换,提高用户态的CPU占比是性能优化的一个目标。##进程、线程、协程在linux内核中,进程和线程是同一个系统调用(克隆),进程和线程的区别:线程共享存储空间,每个执行流都有一个执行控制结构,这里会有一个表面上的指针,指向地址空间结构,一个进程中的多个线程可以通过指向同一个地址结构共享同一个虚拟地址空间。通过fork创建子进程时,不会立即复制数据,而是要等到子进程重写地址空间后才会复制数据。这是有道理的。这是COW(写时复制)。在应用开发中,也有很多类似的引用。协程是用户态下的多执行流。C语言提供了makecontext/getcontext/swapcontext系列接口。很多协程库也是基于这些接口实现的。微信的libco通过hook慢速系统调用(如write、read)代换来实现静默,非常巧妙。##LinkC/C++源码编译链接生成可执行程序,其中数据和代码是分段存放的,我们写的函数会进入文本段,全局数据会进入数据段,未初始化的全局变量会进入bss和heap栈的增长方向相反。局部变量在栈上,参数和返回值也是通过栈传递的。如果想让程序跑得更快,最好把相互调用和密切相关的函数放在代码段附近,这样可以提高icache的命中率。减少代码量,减少函数调用,减少函数指针也可以提高i-cache的命中性能。内联不仅避免了栈帧创建和撤销的开销,也避免了控制跳转刷新i-cache,因此有利于性能。同样,关键路径上的性能敏感函数也应避免递归函数。减少函数调用(就地扩展)与封装相反。有时,为了性能,我们不得不打破封装,破坏可读代码。这是一个权衡利弊的问题。##常识和数据CPU拷贝数据一般可以达到每秒几百兆。当然,每次复制的数据长度不同,吞吐量也不同。如果一个函数执行超过1000个周期,就会比较大(不包括调用子函数的开销)。第一次解锁pthread_mutex_t大约需要4000-5000个周期。之后,每次解锁大约需要120个周期。O2优化的时候,需要100个周期,自旋锁用的时间稍微少一点。锁定内存总线+xchg需要50个周期,内存屏障需要50个周期。有一些无锁技术,比如linux内核中的kfifo,主要是利用整数包装+内存屏障。#4.如何做性能优化(TODO)两个方向:提高运行速度+减少计算量。性能优化和监控必须基于数据或猜测,并构建尽可能模拟真实运行状态的压力测试环境。在此基础上获得的剖析数据是有用的。方法论:监控->分析->优化三部曲#五、几个具体问题(TODO)1.如何定位CPU瓶颈?2、如何定位IO瓶颈?3、如何定位网络瓶颈?4.如何定位锁问题?大家都知道锁会引入额外的开销,但是锁的开销是多少呢?估计很多人都没有测过。我可以给你一个数据。一般一次加解锁100个循环比较快,spinlock或者cas比较快。在使用锁的时候要注意锁的粒度,但是锁的粒度越小越好,太大会增加锁碰撞的概率,太小会让代码更难写。在多线程场景下,如果CPU使用率不增加,系统吞吐量不增加,那么可能是因为锁导致性能下降。这时候可以观察程序的syscpu和usrcpu。这时候,如果你通过perf发现lock的开销很高,那你就对了。5.如何改进?并发??#六、实战经验与案例分析(TODO)内容很多,需要说一下,这里提纲TODO#七、性能优化黑科技人#八、总结还是没想一想,TODO
