摘要: 性能优化指在不影响零碎运行正确性的前提下,使之运行得更快,实现特定性能所需的工夫更短,或领有更弱小的服务能力。
一、思维导图
二、什么是性能优化?
性能优化指在不影响零碎运行正确性的前提下,使之运行得更快,实现特定性能所需的工夫更短,或领有更弱小的服务能力。
关注
不同程序有不同的性能关注点,比方科学计算关注运算速度,游戏引擎重视渲染效率,而服务程序谋求吞吐能力。
服务器个别都是可程度扩大的分布式系统,零碎解决能力取决于单机负载能力和程度扩大能力,所以,晋升单机性能和晋升程度扩大能力是两个次要方向,实践上零碎程度方向能够有限扩大,但程度扩大后往往导致通信老本飙升(甚至瓶颈),同时面临单机解决能力降落的问题。
指标
掂量单机性能有很多指标,比方:QPS(Query Per Second)、TPS、OPS、IOPS、最大连接数、并发数等评估吞吐的指标。
CPU 为了进步吞吐,会把指令执行分为多个阶段,会搞指令 Pipeline,同样,软件系统为了晋升解决能力,往往会引入批处理(攒包),跟 CPU 流水线会引起指令执行 Latency 减少一样,随同着零碎负载减少也会导致提早(Latency)减少,可见,零碎吞吐和提早是两个抵触的指标。
显然,过高的提早是不能承受的,所以,服务器性能优化的指标往往变成: 谋求可容忍提早(Latency)下的最大吞吐(Throughput)。
提早(也叫响应工夫:RT)不是固定的,通常在一个范畴内稳定,咱们能够用均匀时延去评估零碎性能,但有时候,均匀时延是不够的,这很容易了解,比方 80% 的申请都在 10 毫秒以内失去响应,但 20% 的申请时延超过 2 秒,而这 20% 的高提早可能会引发投诉,同样不可承受。
一个改良措施是应用 TP90、TP99 之类的指标,它不是取均匀,而是需确保排序后 90%、99% 申请满足时延的要求。
通常,执行效率(CPU)是咱们的重点关注,但有时候,咱们也须要关注内存占用、网络带宽、磁盘 IO 等,影响性能的因素很多,它是一个简单而乏味的问题。
三、基础知识
能编写运行正确的程序不肯定能做性能优化,性能优化有更高的要求,这样讲并不是想要吓阻想做性能优化的工程师,而是捕风捉影讲,性能优化既须要扎实的零碎常识,又须要丰盛的实践经验,只有这样,你能力具备 case by case 剖析问题解决问题的能力。
所以,相比间接给出论断,我更违心多花些篇幅讲一些基础知识,我保持认为底层根底是了解并把握性能优化技能的前提,值得破费一些工夫钻研并把握这些根技术。
CPU 架构
你须要理解 CPU 架构,了解运算单元、记忆单元、管制单元是如何既各司其职又相互配合实现工作的。
- 你须要理解 CPU 如何读取数据,CPU 如何执行工作。
- 你须要理解数据总线,地址总线和管制总线的区别和作用。
- 你须要理解指令周期:取指、译指、执行、写回。
- 你须要理解 CPU Pipeline,超标量流水线,乱序执行。
- 你须要理解多 CPU、多外围、逻辑核、超线程、多线程、协程这些概念。
存储金字塔
CPU 的速度和访存速度相差 200 倍,高速缓存是逾越这个鸿沟的桥梁,你须要了解存储金字塔,而这个层次结构思维基于着一个称为局部性原理(principle of locality)的思维,它对软硬件零碎的设计和性能有着极大的影响。
局部性又分为工夫局部性和空间局部性。
缓存
古代计算机系统个别有 L1-L2-L3 三级缓存。
比方在我的零碎,我通过进入 /sys/devices/system/cpu/cpu0/cache/index0 1 2 3 目录下查看。
size 对应大小、type 对应类型、coherency_line_size 对应 cache line 大小。
每个 CPU 外围有独立的 L1、L2 高速缓存,所以 L1 和 L2 是 on-chip 缓存;L3 是多个 CPU 外围共享的,它是 off-chip 缓存。
- L1 缓存又分为 i -cache(指令缓存)和 d -cache(数据缓存),L1 缓存通常只有 32K/64KB,速度高达 4 cycles。
- L2 缓存能到 256KB,速度在 8 cycles 左右。
- L3 则高达 30MB,速度 32 cycles 左右。
- 而内存高达数 G,访存时延则在 200 cycles 左右。
所以 CPU-> 寄存器 ->L1->L2->L3-> 内存 -> 磁盘形成存储层级构造:越凑近 CPU,存储容量越小、速度越快、单位成本越高,越远离 CPU,存储容量越大、速度越慢、单位成本越低。
虚拟存储器(VM)
过程和虚拟地址空间是操作系统的 2 个外围形象。
零碎中的所有过程共享 CPU 和主存资源,虚拟存储是对主存的形象,它为每个过程提供一个大的、统一的、公有的地址空间,咱们 gdb 调试的时候,打印进去的变量地址是虚拟地址。
操作系统 +CPU 硬件(MMU)严密单干实现虚拟地址到物理地址的翻译(映射),这个过程总是缄默的主动的进行,不须要利用程序员的任何干涉。
每个过程有一个独自的页表(Page Table),页表是一个页表条目(PTE)的数组,该表的内容由操作系统治理,虚拟地址空间中的每个页(4K 或者 8K)通过查找页表找到物理地址,页表往往是层级式的,多级页表缩小了页表对存储的需要,命失(Page Fault)将导致页面调度(Swapping 或者 Paging),这个惩办很重,所以,咱们要改善程序的行为,让它有更好的局部性,如果一段时间内访存的地址过于发散,将导致平稳(Thrashing),从而重大影响程序性能。
为了减速地址翻译,MMU 中减少了一个对于 PTE 的小的缓存,叫翻译后备缓冲器(TLB),地址翻译单元做地址翻译的时候,会先查问 TLB,只有 TLB 命失才会查问高速缓存(L1-2-3)。
汇编根底
尽管写汇编的场景越来越少,但读懂汇编仍然很有必要,了解高级语言的程序是怎么转化为汇编语言有助于咱们编写高质量高性能的代码。
对于汇编,至多须要理解几种寻址模式,理解数据操作、分支、传送、管制跳转指令。
- 了解 C 语言的 if else、while/do while/for、switch case、函数调用是怎么翻译成汇编代码。
- 了解 ebp+esp 寄存器在函数调用过程中是如何构建和撤销栈帧的。
- 了解函数参数和返回值是怎么传递的。
异样和零碎调用
异样会导致控制流渐变,异样控制流产生在计算机系统的各个档次,异样能够分为四类:
中断(interrupt): 中断是异步产生的,来自处理器内部 IO 设施信号,中断处理程序分上下部。
陷阱(trap): 陷阱是无意的异样,是执行一条指令的后果,零碎调用是通过陷阱实现的,陷阱在用户程序和内核之间提供一个像过程调用一样的接口:零碎调用。
故障(fault): 故障由谬误状况引起,它有可能被故障处理程序修复,故障产生,处理器将管制转移到故障处理程序,缺页(Page Fault)是经典的故障实例。
终止(abort): 终止是不可复原的致命谬误导致的后果,通常是硬件谬误,会终止程序的执行。
零碎调用:
内核态和用户态
你须要理解操作系统的一些概念,比方内核态和用户态,应用程序在用户态运行咱们编写的逻辑,一旦调用零碎调用,便会通过一个特定的陷阱陷入内核,通过零碎调用号标识性能,不同于一般函数调用,陷入内核态和从内核态返回须要做上下文切换,须要做环境变量的保留和复原工作,它会带来额定的耗费,咱们编写的程序应防止频繁做 context swap,晋升用户态的 CPU 占比是性能优化的一个指标。
过程、线程、协程
在 linux 内核中,过程和线程是同样的零碎调用(clone),过程跟线程的区别:线程是共享存储空间的,每个执行流有一个执行控制结构体,这外面会有一个指针,指向地址空间结构,一个过程内的多个线程,通过指向同一地址构造实现共享同一虚拟地址空间。
通过 fork 创立子过程的时候,不会马上 copy 一份数据,而是推延到子过程对地址空间进行改写,这样做是正当的,此即为 COW(Copy On Write),在利用开发中,也有大量的相似借鉴。
协程是用户态的多执行流,C 语言提供 makecontext/getcontext/swapcontext 系列接口,很多协程库也是基于这些接口实现的,微信的协程库 libco(已开源)通过 hook 慢速零碎调用(比方 write,read)做到静默替换,十分奇妙。
链接
C/C++ 源代码经编译链接后产生可执行程序,其中数据和代码分段存储,咱们写的函数将进入 text 节,全局数据将进入数据段,未初始化的全局变量进入 bss,堆和栈向着相同的方向成长,局部变量在栈里,参数通过栈传递,返回值个别通过 eax 寄存器返回。
想要程序运行的更快,最好把互相调用,关系严密的函数放到代码段相近的中央,这样能进步 icache 命中性。缩小代码量、缩小函数调用、缩小函数指针同样能进步 i -cache 命中性。
内联既防止了栈帧建设撤销的开销,又防止了管制跳转对 i -cache 的冲刷,所以有利于性能。同样,要害门路的性能敏感函数也应该防止递归函数。
缩小函数调用(就地开展)跟封装是相违反的,有时候,为了性能,咱们不得不毁坏封装和伤害可读性的代码,这是一个权衡利弊的问题。
常识和数据
CPU 拷贝数据个别一秒钟能做到几百兆,当然每次拷贝的数据长度不同,吞吐不同。
一次函数执行如果消耗超过 1000 cycles 就比拟大了(刨除调用子函数的开销)。
pthread_mutex_t 是 futex 实现,不必每次都进入内核,首次加解锁大略耗时 4000-5000 cycles 左右,之后,每次加解锁大略 120 cycles,O2 优化的时候 100 cycles,spinlock 耗时略少。
lock 内存总线 +xchg 须要 50 cycles,一次内存屏障要 50 cycles。
有一些无锁的技术,比方 CAS,比方 linux kernel 里的 kfifo,次要利用了整型回绕 + 内存屏障。
四、怎么做性能优化(TODO)
两个⽅向:提⾼运⾏速度 + 缩小计算量。
性能优化监控先⾏,要基于数据⽽⾮基于猜想,要搭建能尽量模仿实在运⾏状态的压⼒测试环境,在此基于上获取的 profiling 数据才是有⽤的。
方法论:监控 -> 剖析 -> 优化 三部曲。
工具:
perf 是 linux 内核自带的 profiling 工具,除之之外还有 gprof,但 gprof 是侵入式的(插桩),编译的时候须要加 -pg 参数,会导致运行变慢(慢很多)。
perf 采集的数据,能够用来生成火焰图,也能够用 gprof2dot.py 这个工具来产生比火焰图更直观的调用图,这些工具就是我常常用的。
gprof2dot.py 链接:https://github.com/jrfonseca/gprof2dot/blob/master/gprof2dot.py
性能优化一个重要准则就是用数据谈话,而不能凭空猜测。
瓶颈点可能有多个,如果不解决最狭隘的瓶颈点,性能优化就不能达到预期成果。所以性能优化之前肯定要先进行性能测试,摸清家底,建设测试基线。
例子:之前做 SIP 协定栈,公司的产品须要进步 SIP 性能。美国的一个团队通过实践剖析,单凭实践剖析认为次要是动态内存调配是次要瓶颈,把内存申请成一大块内存,指针都变成的一大块内存的偏移量,十分难于调试,最初成果也不好。咱们又通过测试剖析的形式重构了程序,性能是它们的五倍。
另外,性能优化要一个点一个点的做,做完一点,马上做性能验证。这样能够防止无用的批改。
五、几个具体问题(TODO)
1. 如何定位 CPU 瓶颈?
CPU 是通常大家最先关注的性能指标,宏观维度有核的 CPU 使用率,宏观有函数的 CPU cycle 数,依据性能的模型,性能规格与 CPU 使用率是相互关联的,规格越高,CPU 使用率越高,然而处理器的性能往往又受到内存带宽、Cache、发热等因素的影响,所以 CPU 使用率和规格参数之间并不是简略的线性关系,所以性能规格翻倍并不能简略地翻译成咱们的 CPU 使用率要优化一倍。
至于 CPU 瓶颈的定位工具,最有名也是最有用的工具就是 perf,它是性能剖析的第一步,能够帮咱们找到零碎的热点函数。就像人看病一样,只晓得症状是不够的,须要通过医疗机器进一步剖析病因,能力隔靴搔痒。
所以咱们通过性能剖析工具 PMU 或者其余工具去进一步剖析 CPU 热点的起因比方是指令数自身就比拟多,还是 Cache miss 导致的等,这样在做性能优化的时候不会走偏。
2. 如何定位 IO 瓶颈?
零碎 IO 的瓶颈能够通过 CPU 和负载的非线性关系体现进去。当负载增大时,零碎吞吐量不能无效增大,CPU 不能线性增长,其中一种可能是 IO 呈现阻塞。
零碎的队列长度特地是发送、写磁盘线程的队列长度也是 IO 瓶颈的一个间接指标。
对于网络系统来讲,我倡议先从内部察看零碎。所谓内部察看是指通过观察内部的网络报文交换,能够用 tcpdump, wireshark 等工具,抓包看一下。
比方咱们优化一个 RPC 我的项目,它的吞吐量是 10TPS,客户心愿是 100TPS。咱们应用 wireshark 抓取 TCP 报文流,能够剖析报文之间的工夫戳,响应提早等指标来判断是否是由网络引起来的。
而后能够通过 netstat -i/- s 选项查看网络谬误、重传等统计信息。
还能够通过 iostat 查看 cpu 期待 IO 的比例。
IO 的概念也能够扩大到过程间通信。
对于磁盘类的应用程序,咱们最心愿看到写磁盘有没有时延、频率如何。其中一个办法就是通过内核 ftrace、perf-event 事件来动静观测零碎。比方记录写块设施的起始和返回工夫,这样咱们就能够晓得磁盘写是否有延时,也能够统计写磁盘工夫消耗散布。有一个开源的工具包 perf-tools 外面蕴含着 iolatency, iosnoop 等工具。
3. 如何定位 IO 瓶颈?
应用程序罕用的 IO 有两种:Disk IO 和网络 IO。判断零碎是否存在 IO 瓶颈能够通过观测零碎或过程的 CPU 的 IO 期待比例来进行,比方应用 mpstat、top 命令。
零碎的队列长度特地是发送、写磁盘线程的队列长度也是 IO 瓶颈的一个重要指标。
对于网络 IO 来讲,咱们能够先应用 netstat -i/- s 查看网络谬误、重传等统计信息,而后应用 sar -n DEV 1 和 sar -n TCP,ETCP 1 查看网路实时的统计信息。ss(Socket Statistics)工具能够提供每个 socket 相干的队列、缓存等详细信息。
更间接的办法能够用 tcpdump, wireshark 等工具,抓包看一下。
对于 Disk IO,咱们能够通过 iostat -x -p xxx 来查看具体设施使用率和读写均匀等待时间。如果使用率靠近 100%,或者等待时间过长,都阐明 Disk IO 呈现饱和。
一个更粗疏的察看办法就是通过内核 ftrace、perf-event 来动静观测 Linux 内核。比方记录写块设施的起始和返回工夫,这样咱们就能够晓得磁盘写是否有延时,也能够统计写磁盘工夫消耗散布。有一个开源的工具包 perf-tools 外面蕴含着 iolatency, iosnoop 等工具。
4. 如何定位锁的问题?
大家都晓得锁会引入额定开销,但锁的开销到底有多大,预计很多人没有实测过,我能够给一个数据,个别单次加解锁 100 cycles,spinlock 或者 cas 更快一点。
应用锁的时候,要留神锁的粒度,但锁的粒度也不是越小越好,太大会减少撞锁的概率,太小会导致代码更难写。
多线程场景下,如果 cpu 利用率上不去,而零碎吞吐也上不去,那就有可能是锁导致的性能降落,这个时候,能够察看程序的 sys cpu 和 usr cpu,这个时候通过 perf 如果发现 lock 的开销大,那就没错了。
如果程序卡住了,能够用 pstack 把堆栈打进去,定位死锁的问题。
5. 如何提⾼ Cache 利用率?
内存 /Cache 问题是咱们常见的负载瓶颈问题,通常可利用 perf 等一些通用工具来辅助剖析,优化 cache 的思维能够从两方面来着手,一个是减少部分数据 / 代码的连续性,晋升 cacheline 的利用率,缩小 cache miss,另一个是通过 prefetch,升高 miss 带来的开销。
通过对数据 / 代码依据冷热进行重排分区,可晋升 cacheline 的无效利用率,当然触发 false-sharing 另当别论,这个须要依据运行 trace 进行深刻调整了;
说到 prefetch,用过的人往往都有一种领会,事实成果比预期差的比拟远,的确无论是数据 prefetch 还是代码 prefetch,不确定性太大,咱们和无线做过一些实际,最终以无线输入预取 pattern,编译器主动插入 prefetch 的计划,成果还算能够。
剩下的,下次咱们接着说!
本文分享自华为云社区《性能之巅:定位和优化程序 CPU、内存、IO 瓶颈》,原文作者:左 X 伟。
点击关注,第一工夫理解华为云陈腐技术~