├── README.md
├── SUMMARY.md
├── first-visit-1
├── nmon.md
└── top.md
├── linux-perf
├── sample1
│ └── sample.c
└── sample2
│ └── mem.c
├── perf-ability.md
├── perf
├── perf-hotspot-1.md
└── pou-xi-de-yuan-li.md
└── performance-arsenal.md
/README.md:
--------------------------------------------------------------------------------
1 | # 陈沁悦的性能分析专栏
2 |
3 | ## 我为什么要写性能分析工具介绍
4 |
5 | 在应用开发过程中,我们经常会遇到这样一类问题:“功能全部实现了,但是应用跑得慢, CPU占用率高,甚至出现宕机,怎么办?“能否高效解决这样的问题,是区分资深工程师和初学者的一块试金石。
6 |
7 | 我搜索了一下网络上关于这个问题的答案,有的说“好好研究算法”, 有的说“多读读Effective系列,学习编程最佳实践”,还有的说“认真做代码静态检查”。
8 |
9 | 这些说法就像你跑到诊所和医生说我生病了,医生劝体质差的人多跑步多锻炼一样,能提高一个应用开发者的内功,对写出高性能的算法有所帮助。
10 |
11 | 但是,如果想又快又省力地解决性能问题,最好的办法还是使用性能采样工具给应用拍个X光片来找到关键性能瓶颈。那么,问题来了,你会正确地给应用程序拍片吗,你能看懂X光片吗?
12 |
13 | ## 作者简介
14 |
15 | 陈沁悦,性能分析工具全栈工程师,在实验室从事了13年的性能分析工具的开发和性能分析工作, 做过性能分析工具开发,测试,PM,架构师,现在则是性能分析工具的用户。在这个专栏里,我将结合自己10多年性能领域的工作经验,精选常用性能分析工具,介绍如何在实战中选择正确的工具和使用这些工具,帮助你实现从普通程序员到资深程序员的进阶。
16 |
17 | 邮件联系:
18 |
19 | hugulas AT 163.com
20 |
21 | 欢迎在github上提交你的问题[hugulas](https://github.com/hugulas)/[**perftools-intro**](https://github.com/hugulas/perftools-intro)。
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 |
3 | * [陈沁悦的性能分析专栏](README.md)
4 | * [开篇-性能分析的能力重要吗](perf-ability.md)
5 | * [性能分析的军火库](performance-arsenal.md)
6 |
7 | ## 性能问题初诊
8 |
9 | * [从随手可得的top开始](first-visit-1/top.md)
10 | * [更好用的nmon](first-visit-1/nmon.md)
11 |
12 | ## Linux Perf教程
13 |
14 | * [用Perf寻找程序中的性能热点](perf/perf-hotspot-1.md)
15 | * [剖析的原理](perf/pou-xi-de-yuan-li.md)
16 |
17 |
--------------------------------------------------------------------------------
/first-visit-1/nmon.md:
--------------------------------------------------------------------------------
1 | # 更好用的nmon
2 |
3 | 你好,我是陈沁悦。
4 |
5 | 上一篇,我通过两篇实例介绍了怎么使用top去给性能问题定性。你有没有在听完课程后尝试玩一下这个工具呢?
6 |
7 | 我们的这门课程是一门实战课程,练习很重要。如果你还没有练习过,我建议你一定要动手做一做。分析性能和写程序一样,动手了才能真的学会。
8 |
9 | 如果你已经动手练习过,你也许会发现top与Windows任务管理器、Mac活动监视器相比,好像少了点什么?
10 |
11 | 嗯,是的,top没有IO的性能数据,也没有网络的性能数据。
12 |
13 | todo加一个小标题
14 |
15 | 那么在Linux 上有没有命令行版本的任务管理器呢?有, 我推荐你使用nmon。nmon的作者是我IBM的同事Nigel。到2019年5月,已经有768000个下载了。(要注意的是,nmon和我的课程一样,都是个人作品,和IBM无关,不代表IBM和IBM管理层的观点、立场和战略,也没有提供或隐含任何保证,无法从 IBM 获取相关的帮助。)
16 |
17 | 对比nmon和Linux上其他主流性能监控工具的界面(比如top和sar),nmon颜值上完胜对手。作为一个工程师、一个码农,你去调性能时,当着测试的面打开nmon,是不是妥妥的黑客范。废话不多说,我们拿nmon来分析一下性能问题,通过实战看看它比top好在哪里?
18 |
19 | **nmon的界面:**
20 |
21 | 
22 |
23 | 从启动界面就可以看到,nmon提供了大量的快捷键。 我们先按下c键,就可以看到**处理器的使用情况了。**
24 |
25 | 
26 |
27 | 我这次实验用的是一台安装了CentOS的物理机器,4核的AMD处理器。在nmon的处理器视图中, 竖线"\|" 把视图分成了两半。左边有5列,分别是CPU,User%,Sys%,Wait%,和Idle。
28 |
29 | * CPU列是处理核的序号;
30 | * User%列表示了用户进程消耗的处理器时间比例;
31 | * Sys%表示了系统空间消耗的处理器时间比例;
32 | * Wait%列表示了处理器处于等待状态的处理器时间比例,比如等待磁盘IO;
33 | * Idle列表示了处理器空闲的时间比例。
34 |
35 | 而右边的界面更有意思,nmon通过颜色和字符组成的柱状图,让我们形象化地了解CPU的使用情况。
36 |
37 | 
38 |
39 | 从我贴的图上,你可以看到,每个处理器是一行,绿色底色的字母U表示用户进程消耗的处理器时间,红色底色的字母S表示系统空间消耗的处理器时间。如果绿色字母U越多,说明用户进程消耗的处理器时间越多。
40 |
41 | 我们从贴图中可以看到在表头下依次有四行,分别显示了四个核的利用率,每行的右侧只有一两个U或者S,可见CPU利用率不高。在四个核的利用率下隔了一行是处理器利用率的平均值。
42 |
43 | 如果我还想同时看**内存,网络和磁盘的性能信息怎么办**?我也记不住那么多快捷键啊,那就按下h键吧。
44 |
45 | nmon相比其他命令行的最大的好处是帮助做得非常友好,所见即所得,你可以一边看帮助,一边尝试不同的选项,立刻看到效果。
46 |
47 | 比如,我在按下m键(memory),在界面的底部就冒出了一块新的信息“Memory and Swap”。这新冒出来的一块内容就是内存和交换区信息。我们来读一下。
48 |
49 | 表格第一行开头显示:Page Size:4KB 表示内存是以4KB为单位分块的, 然后有两列:RAM-Memory和SWAP-Space,分别显示了内存和SWAP分区的使用情况。
50 |
51 | 我的这台Linux有6374.3M内存,还有5531.9M空闲,空闲率是82.1%。SWAP分区是6912M,还有5187M空闲,空闲率75%。
52 |
53 | 
54 |
55 | 我除了想看内存和处理器的性能,还想看**磁盘和网络的\*\***性能**\*\*。**
56 |
57 | 我按照帮助的提示按下了n和d,打开了网络(network)和磁盘(disk)的信息。然后,我再次按下了h,把帮助(help)给关闭了。
58 |
59 | nmon的快捷键几乎都是按照英文单词首字母来的,非常容易记住。按一下首字母快捷键打开响应的窗口,再按一下就关了这个窗口。看下面这张贴图,nmon界面中有处理器信息,有内存信息,有网络性能数据,还有磁盘IO的性能数据。我们已经准备好了,让我们来跑个程序练练手吧。 
60 |
61 | 我们运行例子程序2([https://github.com/hugulas/perftools-intro/blob/master/linux-perf/sample2/mem.c](https://github.com/hugulas/perftools-intro/blob/master/linux-perf/sample2/mem.c)), 通过nmon来看看这个程序慢在哪里。
62 |
63 | ```text
64 | # 编译
65 | gcc mem.c -o mem
66 | # 执行,每次调用间隔100微秒
67 | ./mem 100
68 | ```
69 |
70 | 程序运行一小会儿,我们就会发现nmon输出界面中有三处异常。
71 |
72 | 1. 在处理器区域,处理器Wait%占比特别高,屏幕右侧满满的都是蓝色的W字符。怎么回事?
73 | 2. 在磁盘区域,sdc设备Busy 100%,这块磁盘满负载了,屏幕右侧是满满的黄色W字符,有大量的写操作和小部分读操作。
74 | 3. 在内存区域,内存还剩1.4%,几乎用完了。交换(Swap)分区空闲空间不断在减少, 从刚开始的75%下降到66.7%,并且每秒都在不断减少。
75 |
76 | 内存耗尽,是这三个现象的根本原因。内存耗尽,系统不断地使用交换分区,交换分区所在的硬盘sdc3出现了大量的写操作。因为内存用尽,大量的内存操作不得不在磁盘上进行,处理器不得不等待IO结束,所以处理器都处于等待状态。那我们再来继续追踪是哪个程序有问题吧。 
77 |
78 | 我通过按下“n”和“d"关掉了磁盘和网络视图,然后按下“t"打开了“Top Processes”进程列表视图,在进程列表中,我们可以看到mem程序,Size KB那一列是7400668,它使用了超过740万KB内存,也就是7.4GB内存,而且从Res Data那一列可以看到,绝大部分内存花在了数据部分。mem程序吃光了,我机器上所有的内存。 
79 |
80 | ## 总结
81 |
82 | 通过今天的学习,我们可以发现nmon相比top更容易上手,也更方便定制自己的性能监控界面。更重要的是, nmon不仅可以用于定位处理器和内存的问题,还可以用于定位网络,磁盘等更多方面的性能问题。
83 |
84 | 这里我也给你提一个问题:“top,nmon用于监控性能时,都会对系统性能有一定的影响,怎么控制它带来的影响呢?
85 |
86 |
--------------------------------------------------------------------------------
/first-visit-1/top.md:
--------------------------------------------------------------------------------
1 | # 从随手可得的top开始
2 |
3 | 你好,我是陈沁悦。
4 |
5 | 你可能也会遇到这样的场景:自己写了个程序,编译好后,在Linux上跑一会儿,整个机器就卡住了,没有任何响应,怎么回事?
6 |
7 | 这个时候,作为程序员,你的第一反应是什么?
8 |
9 | * 在Windows上,我会打开任务管理器看看什么地方出问题了;
10 | * 在Linux或者AIX上,我会打开top或者topas看看问题出在哪里;
11 | * 而在Mac上,图形界面爱好者会打开Activity Monitor,而命令行的发烧友会继续使用top。
12 |
13 | 其实你可能没有意识到,当打开这些工具的时候,你已经开始在做性能分析的基础工作了。
14 |
15 | 像任务管理器、top、topas、Activity Monitor这些程序,它们被统称为即时性能分析工具,这种工具共同的特点是:“即时”,它们是十分简单、极为常见的工具,就像一把小刀一样,随身携带,使用方便。
16 |
17 | 不管什么平台,你都可以找到这样的即时性能分析工具,它们界面和使用方式也差不多。你如果精通了其中一个,到其他平台上稍微看看操作说明,上手其他的也不难。
18 |
19 | 通过这些即时性能监控工具,你可以看一看问题大概出现在哪个方面,这样“概要”的查看和确定,也就是最简单的概要性能分析方式。通过概要性能分析,我们可以确定,问题是处理器问题呢,还是内存问题,从而对症下药,找出解决方案。
20 |
21 | 下面我们就来一起以top为例,来实践一下:如何给你的应用做一个概要性能分析,我们来看个例子。
22 |
23 | ## 如何运用你的即时分析工具?
24 |
25 | 我有一个例子程序,放在GitHub上。([https://github.com/hugulas/perftools-intro/blob/master/linux-perf/sample1/sample.c](https://github.com/hugulas/perftools-intro/blob/master/linux-perf/sample1/sample.c))。 你可以下载下来,然后用gcc命令编译,再执行这个程序。
26 |
27 | ```text
28 | # 编译程序
29 | gcc sample.c -o sample
30 | # 执行程序 参数 20000
31 | ./sample 20000
32 | ```
33 |
34 | 我们可以在程序执行起来后,在另外一个终端输入“top”命令,就可以看到top的输出了。
35 |
36 | top的输出反应了当时的系统性能状态。输出分成了上下两部分,上面是系统的性能统计信息,下面是进程的性能信息列表,中间有一个空行隔着的。
37 |
38 | ```text
39 | #################### 系统的性能统计信息 ##############################
40 | top - 03:42:22 up 293 days, 2:06, 2 users, load average: 0.28, 0.07, 0.02
41 | Tasks: 117 total, 2 running, 115 sleeping, 0 stopped, 0 zombie
42 | Cpu(s): 99.3%us, 0.7%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
43 | Mem: 520132k total, 485160k used, 34972k free, 79540k buffers
44 | Swap: 135164k total, 1172k used, 133992k free, 313232k cached
45 |
46 | ################### 进程的性能信息 ################################
47 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
48 | 11649 root 20 0 2064 328 284 R 99.2 0.1 0:19.24 sample
49 | 20 root 20 0 0 0 0 S 0.3 0.0 231:45.18 kworker/0:1
50 | 2677 nobody 20 0 15228 9388 4188 S 0.3 1.8 2:44.69 python
51 | 11613 root 20 0 11868 6184 5408 S 0.3 1.2 0:00.04 sshd
52 | 11650 root 20 0 2768 1912 1656 R 0.3 0.4 0:00.11 top
53 | 1 root 20 0 2964 2036 1936 S 0.0 0.4 0:01.78 init
54 | ```
55 |
56 | 系统的性能统计信息分成五行,我们来逐行解释下。
57 |
58 | 1. 第一行是系统信息: 系统信息包括了当前的系统时间3点42分22秒(03:42:22),“up 293 days,2:06” 当前的系统已经运行了293天2小时6分, 2 users表示当前有2个登录的用户,“load avergage:"后面紧跟着的是1分钟、5分钟、15分钟平均负载,平均负载分别是0.28,0.07,0.02。
59 | 2. 第二行是任务信息。“117 total,2 running, 115 sleeping”表示一共117个任务,2个正在运行,115个睡眠的,“0 stopped, 0 zombie”表示没有停止的进程和僵尸进程。
60 | 3. 第三行是处理器信息。“99.3%us表示用户态进程占用了99.3%的CPU时间,0.7%sy”表示内核占用了0.7%的处理器时间。看到这一行,你应该就看出点问题来了,系统的处理器时间被用满了,难怪我这台只有单核的虚拟机卡得不行。
61 | 4. 第四行是内存信息。“520132k total,485160k used,34972k free”表示有520132k物理内存,已经用了485160k。换算下来我的这台虚拟机一共507M内存,已经用了473M,还剩下34M。79540k buffers表示内核缓存的物理内存量是77M。
62 | 5. 第五行是交换空间的信息。“135164k total, 1172k used, 133992k free”表示交换空间一共135164k,已经用了1172k,还剩下133992k。
63 |
64 | 从系统性能统计信息中,我们已经可以注意到,处理器占用率出现了异常。我们再来通过进程信息列表看每个进程的信息。
65 |
66 | 进程信息列表是一个表格,在我们的输出里,一共有12列:
67 |
68 | * PID(进程号);
69 | * USER(进程拥有者的用户名);
70 | * PR(进程的优先级,数字越小,优先级越高,优先被执行);
71 | * NI(进程优先级的NICE值);
72 | * VIRT(进程占用的虚拟内存值);
73 | * RES(进程占用的物理内存);
74 | * SHR(进程使用的共享内存);
75 | * S(进程的状态,值是R表示running正在运行,值是S表示sleeping休眠中);
76 | * %CPU(进程占有处理器时间百分比);
77 | * %MEM(进程使用的物理内存占总内存的百分比);
78 | * TIME+表示进程启动后使用的总处理器时间;
79 | * COMMAND就是启动进程的命令。
80 |
81 | 默认情况下,这个列表是按照处理器占用率降序排列的。我们可以看到排在第一个的 进程,就是我们刚刚运行的那个sample程序,它占了99.2%的处理器时间,它的处理器占用率太高了,这个应用有处理器利用率方面的问题。
82 |
83 | 如果你想要具体找到哪行代码出了问题,我会在后面的“perf小试牛刀”一篇中,教给你一个最高效的办法。
84 |
85 | 通过这个实例,我们了解了top命令的输出,并找到了应用性能问题所属的领域,你是不是对使用这个工具已经有点把握了呢。那我们不妨再来试试,用top再来找找下一个应用是出了什么问题吧。
86 |
87 | ## 如何找到应用的问题出在哪里?
88 |
89 | 我们可以从GitHub上下载第二个应用的源代码([https://github.com/hugulas/perftools-intro/blob/master/linux-perf/sample2/mem.c](https://github.com/hugulas/perftools-intro/blob/master/linux-perf/sample2/mem.c))。打开第二个应用让它跑一会后,整个系统都不好了,完全不响应了。我不信这个邪,打开了top,然后另外找了一个终端执行程序。
90 |
91 | ```text
92 | # 编译
93 | gcc mem.c -o mem
94 | # 执行,每次调用间隔500000微秒
95 | ./mem 500000
96 | ```
97 |
98 | 通过上一部分的介绍,我们知道如何查看top的输出。我们来检查一下,有哪些异常的数据。
99 |
100 | ~~物理内存占用涨到了359556k~~
101 |
102 | ```text
103 | #################### 系统的性能统计信息 ##############################
104 | top - 11:31:19 up 293 days, 9:55, 2 users, load average: 0.00, 0.00, 0.00
105 | Tasks: 119 total, 1 running, 118 sleeping, 0 stopped, 0 zombie
106 | Cpu(s): 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
107 | Mem: 520132k total, 359556k used, 160576k free, 5076k buffers
108 | Swap: 135164k total, 51256k used, 83908k free, 28732k cached
109 |
110 | ################### 进程的性能信息 ################################
111 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
112 | 13208 root 20 0 2768 1932 1680 R 0.3 0.4 0:00.97 top 1 root 20 0 2964 8 4 S 0.0 0.0 0:01.82 init
113 | 2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
114 | 4 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/0:0H
115 | ```
116 |
117 | 你会发现,在系统性能状态的第四行mem那一行,used那一栏显示的物理内存占用在蹭蹭地往上涨,一眨眼的功夫,内存占用就从跑之前的82704k上涨到了下面输出中的359556k。这是怎么回事呢?
118 |
119 | 我们不妨在top界面按下大写O,来改变进程列表的排序方式。这时,top界面会让我们选择根据哪一列来排序,从下面的列表可以看出,内存占用“%mem”对应的是字母n,我们按下“n”,然后按下回车。
120 |
121 | 屏幕上显示是这样的:
122 |
123 | ```text
124 | Current Sort Field: K for window 1:Def
125 | Select sort field via field letter, type any other key to return
126 |
127 |
128 | a: PID = Process Id
129 | b: PPID = Parent Process Pid
130 | c: RUSER = Real user name
131 | d: UID = User Id
132 | e: USER = User Name
133 | f: GROUP = Group Name
134 | g: TTY = Controlling Tty
135 | h: PR = Priority
136 | i: NI = Nice value
137 | j: P = Last used cpu (SMP)
138 | * K: %CPU = CPU usage
139 | l: TIME = CPU Time
140 | m: TIME+ = CPU Time, hundredths
141 | n: %MEM = Memory usage (RES)
142 | o: VIRT = Virtual Image (kb)
143 | p: SWAP = Swapped size (kb)
144 | q: RES = Resident size (kb)
145 | r: CODE = Code size (kb)
146 | s: DATA = Data+Stack size (kb)
147 | t: SHR = Shared Mem size (kb)
148 | u: nFLT = Page Fault count
149 | v: nDRT = Dirty Pages count
150 | w: S = Process Status
151 | x: COMMAND = Command name/line
152 | y: WCHAN = Sleeping in Function
153 | z: Flags = Task Flags
154 | ```
155 |
156 | 然后,我们会发现top界面中,进程列表的第一个就是我们的刚刚运行的mem小程序,它花的2内存也在短短的几十秒内爬到了271M,大大超出了我对一个不到100行的小程序的预期。
157 |
158 | 这个程序有内存问题,需要进一步的调优。关于如何做内存的分析,我会在后面章节的内存部分介绍。
159 |
160 | ```text
161 | top - 11:34:03 up 293 days, 9:57, 2 users, load average: 0.04, 0.02, 0.00
162 | Tasks: 119 total, 1 running, 118 sleeping, 0 stopped, 0 zombie
163 | Cpu(s): 0.0%us, 0.3%sy, 0.0%ni, 99.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
164 | Mem: 520132k total, 359524k used, 160608k free, 5100k buffers
165 | Swap: 135164k total, 51192k used, 83972k free, 28728k cached
166 |
167 |
168 | PID USER PR VIRT NI RES SHR S %CPU %MEM TIME+ COMMAND
169 | 13212 root 20 271m 0 271m 952 S 0.0 53.4 0:00.87 mem
170 | 13193 root 20 12068 0 6216 5308 S 0.0 1.2 0:00.36 sshd
171 | 13130 root 20 11756 0 6016 5244 S 0.0 1.2 0:00.12 sshd
172 | 13132 root 20 5296 0 2984 2712 S 0.0 0.6 0:00.08 bash
173 | ```
174 |
175 | 通过这两个例子,我们学会了使用top,了解了top界面都有哪些信息,怎么解读这些信息,怎么将进程列表按照某一种信息排序找出问题最突出的进程。
176 |
177 | ## 总结
178 |
179 | 你可以在课后把程序下载下来,自己动手跑一跑,用top分析一下。我想,你很快就能学会top的使用。如果你能熟练地掌握这个工具,遇到性能问题就能很快判断出这大概是哪一种资源用得太多了。
180 |
181 | 这里给你留个思考题,如果用户和我说,你们的应用每天凌晨三点会瘫痪,你怎么办?你是等到凌晨三点一直盯着top吗,还是有更好的办法呢?
182 |
183 |
--------------------------------------------------------------------------------
/linux-perf/sample1/sample.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | struct PrimeNumberNode
5 | {
6 | int prime;
7 | int count;
8 | struct PrimeNumberNode *next;
9 | };
10 |
11 | int wasteTime(int factor)
12 | {
13 | int i = 0;
14 | int testResult;
15 | for (i = 0; i < factor; i++)
16 | {
17 | testResult += i;
18 | testResult = testResult * 1;
19 | }
20 | return testResult;
21 | }
22 |
23 | // function to get prime number which is very hot
24 | struct PrimeNumberNode *number(int input)
25 | {
26 | int i;
27 | int j;
28 |
29 | struct PrimeNumberNode *head = (struct PrimeNumberNode *)malloc(sizeof(struct PrimeNumberNode));
30 | struct PrimeNumberNode *currentNode = head;
31 | for (i = 3; i < input; i++)
32 | {
33 | if (i % 2 == 1)
34 | {
35 | for (j = 2; j < i; j++)
36 | {
37 | int testResult = wasteTime(j);
38 | if (testResult % 10000 == 9999)
39 | printf("Test code");
40 | if (i % j == 0)
41 | continue;
42 | }
43 | struct PrimeNumberNode *primeNode = (struct PrimeNumberNode *)malloc(sizeof(struct PrimeNumberNode));
44 | currentNode->next = primeNode;
45 | currentNode = primeNode;
46 | head->count++;
47 | }
48 | }
49 |
50 | return head;
51 | }
52 |
53 | int main(int argc, char *argv[])
54 | {
55 | printf("input=%d\n", atoi(argv[1]));
56 | struct PrimeNumberNode* head = number(atoi(argv[1]));
57 | printf("%d\n",head->count);
58 |
59 | head = number(atoi(argv[1]));
60 | printf("%d\n",head->count);
61 | }
62 |
--------------------------------------------------------------------------------
/linux-perf/sample2/mem.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | int main(int argc, char *argv[])
6 | {
7 | int n = atoi(argv[1]);
8 | int enough = 1;
9 | while(1)
10 | {
11 |
12 | // use 1MB memory
13 | if (enough) {
14 | void *m = malloc(1024*1024);
15 | if (!m) {
16 | enough = 0;
17 | printf("No more memory to use");
18 | }
19 | else {
20 | memset(m,0,1024*1024);
21 | }
22 | }
23 | // sleep n micro seconds
24 | usleep(n);
25 | }
26 | return 0;
27 | }
28 |
--------------------------------------------------------------------------------
/perf-ability.md:
--------------------------------------------------------------------------------
1 | # 开篇-性能分析的能力重要吗
2 |
3 | 在开篇的第一课,我想先向你提一个问题:“**性能重要吗**?” 不出意外,我想,几乎所有的计算机从业者都会不假思索给我一个统一的答案:“重要”。**为什么我们都觉得性能重要呢?**
4 |
5 | 从我们迈入软件行业开始,性能的重要性就一再被强调。
6 |
7 | 在读书时,我们学习了大量的计算机专业课程,花了大量的时间去讨论怎么让我们计算机系统获得比较好的性能。
8 |
9 | 我们可以回忆一下大学四年中那些专业课,数据结构、离散数学、算法、计算机组成原理、数据库、网络、C/C++、Java等等,关于性能的讨论无处不在,就连软件工程都有讨论性能的章节(我能想到的唯一的例外是知识产权法)。这些性能方面的专业知识为计算机和软件专业的学生在从事编程工作时提供了核心竞争力。
10 |
11 | 而当我们去找工作时,大部分的公司在笔试和面试时都会考一考算法,问一问算法复杂度。能写出比别的竞争者时间复杂度更低、性能更好的程序是应征者获得一份工作的关键。
12 |
13 | 等到工作后,公司的领导和同事又会怎么评估你作为一个程序员的编程水平,怎么评估你的作品呢?
14 |
15 | 当然,我们需要考虑很多方面,比如代码的质量、可维护性等。这些都很重要,但是这些很多都是背后的功夫,不好衡量。唯有性能方面的成就是直接可以量化的。
16 |
17 | 代码性能好不好,不用吹,跑一跑,领导和客户都能看到。“隔壁组的王同学重构了消息队列代码,解决了系统宕机问题,交易的平均延时减少了50%,吞吐率增加了100%”。
18 |
19 | 这样的功绩一目了然,表扬信会从客户经过各级领导一级一级转发到CTO。每次对内对外的各种报告会讲座,王同学都会把这件事情拿来讲一讲。解决关键性能瓶颈就和世界杯最后1分钟打进决胜球一样痛快,充满了职业成就感。这样的成绩你是不是很羡慕呢?
20 |
21 | 上面我说的都是一些感性的认识。我们能不能通过量化的方式来看一看**性能到底有多重要呢**?答案是可以的。性能对于企业来说即是钱,也是命。性能是钱,应用的性能越好,使用应用的企业和个人用户就能在硬件和电费上少花钱。性能是命,商用软件的性能达不到合同的要求,客户不会接受;手机应用性能不达标,用户会流失,会卸载。
22 |
23 | 从钱的角度去量化性能的重要性非常容易,打个比方,隔壁组王同学通过代码调优让应用少用了10%的处理器资源。对于不同的应用,这10%的价值是不一样的。
24 |
25 | * 如果这是一款Mac OSX上跑的畅销游戏,王同学可能为每个用户省了1000多块。为什么这么说呢?我查了下苹果官网,MacBook Pro 15寸2019版提供了两款不同处理器主频供用户选择,2.4GHz高配版比2.3GHz低配版贵了1468人民币,但是在GeekBench Browser上公布的处理器跑分高配版却只强了5.3%。
26 |
27 | **苹果官网上15寸MacBook Pro的处理器可选包:**\([https://www.apple.com/cn/shop/buy-mac/macbook-pro/MV912CH/A\#](https://www.apple.com/cn/shop/buy-mac/macbook-pro/MV912CH/A#)\)
28 |
29 | **GeekBench的跑分:**\([https://browser.geekbench.com/macs/446](https://browser.geekbench.com/macs/446)\) 
30 |
31 | * 如果这是一款跑在云计算上的商业应用,这10%的性能优化每个小时都在点点滴滴地为企业节约着运营成本。我查了下亚马逊EC2云服务的报价,计算密集型的c5实例每小时每颗处理器报价是0.054美元,假设企业原来用1000颗c5虚拟处理器,应用10%的处理器开销减少大约减少了100颗处理器,每小时省5.4美元,每年省47304美元,大约是32万人民币。企业的业务越多,规模越大,10%的性能优化带来的每年的成本节约也越大。
32 |
33 | **AWS上按需计费的计算优化实例c5d:**\([https://aws.amazon.com/cn/ec2/pricing/on-demand/](https://aws.amazon.com/cn/ec2/pricing/on-demand/)\) 
34 |
35 | 看到这里,你可能会拍拍钱包轻蔑地一笑,“我们公司不差钱, 有钱。花钱上新机器,一台不够买两台”。
36 |
37 | 不过,你忽略了一点。在过去的几十年里,是投资硬件还是投资性能调优,一直是一道困难的选择题。但是在摩尔定律趋近于失效的今天,通过投资更快更好的硬件来实现硬件提升,正变得越来越难,也越来越贵。
38 |
39 | Linus大神在2019年上海的kubenetes和Cloud Native大会上就表示:软件开发正变得越来越困难。"在过去,摩尔定律保证了硬件性能每18个月增加一倍。但是因为处理器供应商已经逼近了摩尔定律的极限,众多开发者将来必须通过调优软件来获得更高的性能。“
40 |
41 | > “Torvalds said. Moore's Law has guaranteed a doubling of hardware performance every 18 months for decades. But as processor vendors approach the limits of Moore's Law, many developers will need to reoptimize their code to continue achieving increased performance. In many cases, that requirement will be a shock to many development teams that have counted on those performance improvements to make up for inefficient coding processes, he said.“
42 |
43 | **这也意味着,在未来的十年里,软件性能分析的技能将变得越来越重要,越来越吃香。**
44 |
45 | 与学习一门具体的编程语言,学习某个正在流行的框架不同的是,这些方法和原理在过去十年和未来十年都会一直有效,不会轻易被快速发展的技术潮流所轻易淘汰,保值性非常好。
46 |
47 | 它是计算机领域稀有的一门越老越值钱的技能,它能帮助你保持职业生涯中的技术竞争力。
48 |
49 | TODO加上一些“我”的工作经历。
50 |
51 | (我的同事中不乏60岁,65岁的软件性能分析师)。你还犹豫什么,赶快点亮性能分析这个技能吧。
52 |
53 | 我将会在这门性能课程中带你学习怎么分析应用程序的性能。在本课,我希望通过实例带着你学会解决这三个性能分析的问题: 1. 怎么描述一个性能问题?性能问题出在哪个方面? 2. 知道了性能问题的大致方向,怎么找到合适的工具去分析性能问题? 3. 怎么使用这些工具去采集性能数据?怎么解读数据寻找性能瓶颈?
54 |
55 | 更进一步的是,我们会解释这些工具背后的性能分析原理。哪怕不断有新的工具出现,领悟了原理的你也可以在短时间内上手。
56 |
57 | 在本篇的结尾,我留下一个思考题:“应用的性能调优除了能为企业省钱,能满足软件的性能需求外,对于企业,对于用户,对于开发者,还有什么额外的意义呢?”
58 |
59 | 我的答案:“我觉得这还是一份情怀。性能优化除了减少处理器开销外,另外一个副作用就是省电减排。这也算是作为程序员为蓝天白云永驻做得一点微薄的贡献吧。”
60 |
61 |
--------------------------------------------------------------------------------
/perf/perf-hotspot-1.md:
--------------------------------------------------------------------------------
1 | # 用Perf寻找程序中的性能热点
2 |
3 | 你好,我是hugulas chen。
4 |
5 | 作为程序员,在软件开发过程中,我们有的时候会遇到一些棘手的性能问题。比如下面的两种情况:
6 |
7 | * 运维报告说:新上线的应用变慢了,处理器利用率也变高了。
8 | * 用户对我们抱怨说:安装新应用后,跑到某个环节就卡住,系统都不响应了。
9 |
10 | 这个时候,你该怎么办呢?
11 |
12 | 在刚学编程的时候,我会去猜哪一段代码有性能问题,然后挨个给每个函数、每个循环加上计数器,通过计数器的值判断程序到底慢在哪里。但是过去的经验告诉我,这样的做法非常低效。
13 |
14 | 我花了大量的时间去做代码静态分析,调试计数器代码和反复跑慢得快宕机的代码,也不一定能猜到正确答案。而且要注意的是,这些插入到函数和循环中的计数器可能会带来非常大的性能负面影响,这些负面影响也会误导我们。
15 |
16 | **既然上述操作行不通,这里\*\***我**来**告诉你一个更好的办法:采样。\*\*
17 |
18 | 采样是我们遇到“处理器消耗大,程序运行慢”时,首先应该想到的办法,它最快、最高效、负面影响也相对较小。你不需要改动应用代码,仅需对正在运行的应用做一次或者两次的采样,就能迅速找到性能的热点。
19 |
20 | 不同的平台提供了不同的采样工具。Linux平台上最著名的采样工具是perf和OProfile,它们都是Linux平台上轻量化的采样工具。采样给系统和应用带来的性能开销是可控的,比向每个函数每个循环插入计数器要小的多。
21 |
22 | perf诞生在2009年,诞生在OProfile之后。当时,OProfile已经流行起来。但是,OProfile是一个独立的工具,它的更新相对来说比较慢,跟不上Linux的发展脚步,而且开发流程也和Linux内核团队不同。所以,林纳斯(Linus)就引入了perf这样一个工具。perf的代码和Linux内核代码放在一起,是内核级的工具。
23 |
24 | perf的开发比OProfile要活跃得多,紧紧跟着Linux内核发布的节奏,对Linux内核的支持要比OProfile好得多。而且在功能上,perf也更强大,可以对众多的软硬件事件采样,还能采集出跟踪点(trace points)的信息(比如系统调用、TCP/IP事件和文件系统操作。和我一起的很多小伙伴都转向了使用perf)。所以,我觉得perf是在Linux上做剖析分析的首选工具。
25 |
26 | 那么,今天我们就通过解决“程序CPU占用高”这一问题,来看看perf是如何帮我们解决性能问题的。
27 |
28 | ### perf初体验
29 |
30 | 首先,我们看一看怎么用perf这把“牛刀”找到代码中的性能热点。
31 |
32 | 为了让你更容易理解,我用C写了一个求质数的小程序。你运行了之后,一定会偷偷问我:“你这程序怎么这么慢啊?一跑起来CPU利用率就是100%。”
33 |
34 | 别着急,我会在接下来的时间里带着你一起,用Perf找到这个小程序慢的根源。(你可以在GitHub上下载到这个程序。)[https://github.com/hugulas/perftools-intro/blob/master/linux-perf/sample1/sample.c](https://github.com/hugulas/perftools-intro/blob/master/linux-perf/sample1/sample.c)
35 |
36 | #### 第一步:在Linux上安装perf
37 |
38 | 第一步当然是安装perf。有些比较早期的介绍perf的文档,会建议用户通过编译perf源代码来安装,但现在绝大部分主流的Linux发行版都提供了perf的支持,不管是Redhat系列,SUSE系列还是Debian系列,安装perf都只是一条命令的事了。
39 |
40 | 我在这里总结了不同Linux发行版上安装perf的命令,需要的同学可以加到笔记。
41 |
42 | * Cent OS/RHEL:yum install perf
43 | * Fedora:dnf install perf
44 | * SUSE:zypper install perf
45 | * Ubuntu:apt install linux-tools-common
46 |
47 | 和OProfile相比,安装简单是一个巨大的优势,90%以上的用户再也不需要为了获得内核部分的采样,而被迫编译相关代码了。在perf出现以前,我不止一次卡在OProfile内核支持的编译上。(我猜愤怒的林纳斯或许也有过这种不堪回首的经历。)
48 |
49 | #### 第二步:通过perf stat判断程序是不是真的CPU占用率很高
50 |
51 | 我们在做性能分析时,有一点要牢记,所有的结论都应该由数据来支持,而不是直接依赖他人的判断。所以,我们首先需要**判断这个程序是不是真的很慢,是不是真的非常消耗处理器资源。**
52 |
53 | 我们可以通过执行下面这条命令,来采集程序的运行时间和CPU开销:
54 |
55 | ```text
56 | perf stat ./sample 2000
57 | ```
58 |
59 | 我先来解释一下这条命令,它由三部分组成。
60 |
61 | * perf:perf命令,perf命令的风格和版本控制工具git很像,它提供了非常多的子命令,比如stat、top、record、annotate等。通过调用这些不同的子命令,用户可以完成性能采样、性能分析、可视化等不同任务。
62 | * stat:stat子命令,它汇总了程序执行过程中处理器和系统的一些计数器信息。它就好像程序的验血单,通过这张验血单,性能分析师就能判断出应用到底有没有生病,问题可能在哪里。
63 | * ./sample 2000:被采样的命令,./sample是我写的小程序,2000是它的参数,表示寻找2000以下的所有质数。通过这一部分,我们告诉了perf怎么执行被采样的应用。
64 |
65 | 接着,我们再来看下刚才那条perf stat命令的输出。在文章中,我贴出来这条命令的输出:
66 |
67 | ```text
68 | # 运行perf命令
69 | bash# perf stat ./sample 2000
70 | # Performance counter stats for './sample 2000':
71 |
72 |
73 | 5462.107517 task-clock (msec) # 0.998 CPUs utilized
74 | 91 context-switches # 0.017 K/sec
75 | 0 cpu-migrations # 0.000 K/sec
76 | 45 page-faults # 0.008 K/sec
77 | cycles
78 | stalled-cycles-frontend
79 | stalled-cycles-backend
80 | instructions
81 | branches
82 | branch-misses
83 |
84 |
85 | 5.474573155 seconds time elapsed
86 | ```
87 |
88 | 输出的最后一行显示:求质数的命令执行了5.47秒。我的小程序运行了5.47秒,才找到所有2000以下的质数,的确是够慢的。
89 |
90 | 输出中还显示了task-clock \(msec\)是5462毫秒,也就是5.462秒,这是这个任务在处理器上消耗的时间。这个单线程程序的处理器时间5.462秒和总运行时间5.47秒几乎一样,处理器利用率是99.8%,这个处理器占用率非常高。
91 |
92 | 那么,是什么原因让它这么慢,这么消耗CPU的呢?要了解这个问题,我们接下来要对它做剖析,如果你看的是英文资料,对应的术语是Profiling, 顾名思义也就是给应用拍个X光片。
93 |
94 | #### **第三步:剖析采样**
95 |
96 | 剖析采样可以帮助我们采集到程序运行的特征,而且剖析精度非常高,可以定位到具体的代码行和指令块。关于剖析的原理和注意事项,我会在下一篇文章中详细介绍。
97 |
98 | 我执行了下面这条perf record命令,来采集程序的剖析信息。record子命令会对系统进行采样,并把采样结果保存在perf.data文件中。
99 |
100 | 我通过“-F 999”选项,我把采样频率设置为999Hz,每秒采样999次。对运行5秒的程序,大约会有5000次采样。你不妨思考一下,为什么我要用999而不是1000呢?
101 |
102 | ```text
103 | perf record -F 999 ./sample 2000
104 | ```
105 |
106 | 我把采样频率设置为999而不是整数,是一个实践中得出的小技巧。因为系统或者应用可能会有一些周期性的行为,一般是准点执行的。如果我使用1000等整数频率,可能会经常遇到某些周期性行为,而破坏了采样的随机性,让结果失真。
107 |
108 | 另外,采样时,我们要注意的是采样频率设置得越高,采样行为给系统带来的性能干扰越大;而如果设置得过小,那么采样的精确性就无法保证。所以,我一般会尽量保证我关心的函数和代码行有二位数以上的采样点,否则我就会调整采样频率重新采样。
109 |
110 | perf有如下两行输出结果。从perf的输出,我们知道采样结果被写到了“perf.data”文件中。
111 |
112 | ```text
113 | [ perf record: Woken up 1 times to write data ]
114 | [ perf record: Captured and wrote 0.217 MB perf.data (5520 samples) ]
115 | ```
116 |
117 | #### **第四步:通过\*\***p**erf** r**\*\*eport快速定位性能瓶颈**
118 |
119 | 采样结束后,性能分析师就可以通过perf report命令寻找采样中的性能瓶颈了。
120 |
121 | 下面是perf report的输出。
122 |
123 | ```text
124 | Samples: 5K of event 'cpu-clock', Event count (approx.): 5525525520
125 | Overhead Command Shared Object Symbol
126 | 99.06% sample sample [.] wasteTime
127 | 0.69% sample sample [.] number
128 | 0.07% sample [kernel.kallsyms] [k] __do_softirq
129 | 0.04% sample [kernel.kallsyms] [k] _raw_spin_unlock_irqrestore
130 | 0.04% sample [kernel.kallsyms] [k] run_timer_softirq
131 | 0.04% sample libc-2.12.so [.] malloc
132 | 0.02% sample [kernel.kallsyms] [k] _raw_spin_lock_irq
133 | 0.02% sample [kernel.kallsyms] [k] cursor_timer_handler
134 | 0.02% sample [kernel.kallsyms] [k] finish_task_switch
135 | 0.02% sample libc-2.12.so [.] _int_malloc
136 | ```
137 |
138 | perf report的输出是一个表格,依次有Overhead, Command, Shared Object, Symbol五列。
139 |
140 | * Overhead:指出了该Symbol采样在总采样中所占的百分比。在当前场景下,表示了该Symbol消耗的CPU时间占总CPU时间的百分比
141 | * Command:进程名
142 | * Shared Object:模块名, 比如具体哪个共享库,哪个可执行程序。
143 | * Symbol:二进制模块中的符号名,如果是高级语言,比如C语言编写的程序,等价于函数名。
144 |
145 | 从perf report的输出中,我们可以看到,在采样的5秒多时间内,sample模块的wasteTime函数花费了最多的处理器时间,99.06%的采样都落在了这个函数上。毫无疑问,这个函数是性能瓶颈。就像它的名字wasteTime一样,是我预先埋下的,用来浪费处理器时间的函数,你优化它就可以大大提高应用性能。
146 |
147 | 但是只定位到函数还不够好,perf工具还能帮我们定位到更细的粒度,这样我们就不用去猜函数中哪一段代码出了问题。如果我们通过键盘上下键把光标移动到wasteTime函数上,然后敲击Enter键,perf给出了一些选项。通过这些选项,我们可以进一步分析这个函数。
148 |
149 | 我们选中第一个选项“Annotate wasteTime”,我们敲击Enter键就可以对函数做进一步分析了。
150 |
151 | **点击WasteTime后显示了下面的菜单:**
152 |
153 | ```text
154 | Annotate wasteTime --- 分析wasteTime函数中指令或者代码的性能
155 | Zoom into sample(32477) thread --- 聚焦到线程 sample(32477)
156 | Zoom into sample DSO --- 聚焦到动态共享对象sample(32477)
157 | Browse map details --- 查看map
158 | Run scripts for samples of thread [sample]--- 针对sample线程的采样运行脚本
159 | Run scripts for samples of symbol [wasteTime] --- 针对函数的采样运行脚本
160 | Run scripts for all samples --- 针对所有采样运行脚步
161 | Switch to another data file in PWD --- 切换到当前目录中另一个数据文件
162 | Exit
163 | ```
164 |
165 | 这里的Annotate是perf另外一个子命令,可以用于分析指定函数内(更精确地说应该是Symbol内)指令或者代码行的处理器开销。
166 |
167 | 下面是“Annotate wasteTime"的结果,Annotate输出是一个表格。整个表格被竖线分成左右两栏,竖线左边是每条指令的CPU-Clock占总体的百分比,也就是每条指令的热度。竖线的右边是指令的源代码行,反汇编代码等信息。
168 |
169 | **wasteTime的Annotate的输出:**
170 |
171 | ```text
172 | │ Disassembly of section .text:
173 | │
174 | │ 08048424 :
175 | │ wasteTime():
176 | 0.04 │ push %ebp
177 | │ mov %esp,%ebp
178 | 0.02 │ sub $0x10,%esp
179 | │ movl $0x0,-0x8(%ebp)
180 | │ movl $0x0,-0x8(%ebp)
181 | │ ┌──jmp 20
182 | 22.31 │16:│ mov -0x8(%ebp),%eax
183 | 0.34 │ │ add %eax,-0x4(%ebp)
184 | 26.32 │ │ addl $0x1,-0x8(%ebp)
185 | 7.96 │20:└─→mov -0x8(%ebp),%eax
186 | 42.54 │ cmp 0x8(%ebp),%eax
187 | │ ↑ jl 16
188 | 0.24 │ mov -0x4(%ebp),%eax
189 | 0.15 │ leave
190 | 0.08 │ ret
191 | ```
192 |
193 | 我们上面贴出来的输出中只包含了wasteTime函数反汇编后采样在每条指令上的分布。我的小程序是C语言编写,通过编译后就变成了一段段的二进制指令,比如wasteTime函数就是由一堆指令所组成的。 perf annnoate报告中为了方便我们阅读这些指令,就对这些指令做了反汇编,编成了我们可以阅读的反汇编代码。
194 |
195 | 如果这是一条跳转指令(跳转指令改变了指令的顺序,高级语言的循环、判断语句都依赖跳转指令来完成),在跳转指令前,会显示↑↓上下箭头符号,指明指令跳转的方向,比如“jmp 20”指令就是向下跳的。如果把光标移到跳转指令“jmp 20”指令上面,perf界面上会和output中一样,通过箭头链接显示出“jmp 20”跳转目标:指令“mov -0x8\(%ebp\),%eax”。我们会注意到"jmp 20"中有个数字20,在它的跳转目标“mov”指令前也有一个20. 这个20是什么意思呢?20是个十六进制数,这是指令的偏移地址,表示这条指令在函数起始地址的0x20位置。“jmp 20”表示跳转到函数起始位置的+0x20位置。如果我们想看到这些指令的绝对地址,而不是相对地址,大家可以按下o来切换到绝对地址模式。
196 |
197 | 在wasteTime的Annotate的输出中,我们可以看到指令0x16-0x20附近是最热的代码。我们可以通过改变16-20行的汇编代码来提高性能,比如修改汇编或者使用不同的编译选项。但是,仅仅找到最消耗处理器资源指令行对于大部分应用开发者来说还不够,我们很难通过猜测这些指令和代码相关联。有没有办法让perf帮我们找到对应的源代码行呢呢?答案是可以的,我们需要执行第五步
198 |
199 | #### **第五步:定位性能瓶颈的代码行**
200 |
201 | 我们在第四步时,没有办法通过perf annotate看到代码行的采样信息,是因为我们采样的程序没有调试信息。我们需要重新编译代码来给程序附上调试信息。
202 |
203 | 一般情况下,大部分编译器都只需要在编译时加上相应的选项。比如对于gcc编译器,我们只要在编译时加上“-g"选项,编译产生的模块就会包含调试信息。我们就可以在perf annotate子命令产生的报表中直接看到相关源代码了。
204 |
205 | 我们执行下面的命令来重新进行编译和采样,来给应用加上调试信息:
206 |
207 | ```text
208 | # 通过-g选项重新编译sample.c, 输出的二进制程序为sample_with_source
209 | gcc -g sample.c -o sample_with_source
210 | # 通过perf record命令对sample_with_source程序采样
211 | perf record -F 999 ./sample_with_source 2000
212 | ```
213 |
214 | 然后,我们通过单独执行命令perf annotate --stdio --symbol=wasteTime来打印wasteTime函数的采样信息。
215 |
216 | 参数--symbol=wasteTime指明了要分析对象是符号wasteTime。 这条命令等价于我们在上文第四步中通过perf界面执行“Annoate wasteTime”菜单。perf会把选中函数的代码行,每一行代码对应的指令,每条指令的采样数都打印出来,因为重新编译了的sample\_with\_source程序中包含了调试信息,我们就可以直接在输出中看到wasteTime函数的代码行以及这些代码行关联了哪些指令。
217 |
218 | 比如下面的wasteTime函数的代码行信息输出:wasteTime中的for循环语句是整个函数中采样最多的代码行。它编译后产生的8048440到804844a的这几条指令花费了绝大部分的处理时间。 这行代码就是性能的瓶颈。
219 |
220 | ```text
221 | perf annotate --stdio --symbol=wastTime
222 | : int wasteTime(int factor)
223 | : {
224 | : int i = 0;
225 | : int testResult;
226 | : # 循环是性能瓶颈
227 | : for (i = 0; i < factor; i++)
228 | 26.72 : 8048440: addl $0x1,-0x8(%ebp)
229 | 7.69 : 8048444: mov -0x8(%ebp),%eax
230 | 43.81 : 8048447: cmp 0x8(%ebp),%eax
231 | 0.00 : 804844a: jl 804843a
232 | : {
233 | : testResult += i;
234 | : testResult = testResult * 1;
235 | : }
236 | : return testResult;
237 | 0.24 : 804844c: mov -0x4(%ebp),%eax
238 | : }
239 | 0.09 : 804844f: leave
240 | 0.04 : 8048450: ret
241 | ```
242 |
243 | ### 总结
244 |
245 | 今天我们学习了如何使用Perf去抓住代码中的性能瓶颈。首先,我们通过perf stats命令对程序的性能特点有了基本的了解,确认了程序存在处理器利用率高的问题;然后,我们通过perf record命令对程序进行了采样;最后,我们使用perf report命令分析采样,找到了代码中的性能热点。
246 |
247 | 我建议你可以在课后找台Linux安装好perf,按照我们今天所做的步骤,亲自动手使用perf做一做性能分析。欢迎和我讨论你在性能分析过程中所遇到的问题。
248 |
249 | 现在,我们知道perf是Linux平台上的做性能分析的利器,可以在很短时间内帮我们找到程序中的处理器开销最大的代码。它相比其他工具,安装起来更简单,操作也很方便,如果你能熟练操作git,一定也能熟练操作它。掌握好这个工具,你就可以不断压榨你的程序的性能, 写出更流畅,更省电的应用。我自己非常喜欢这样的感觉,我曾经把一个跑20分钟的数据分析应用通过剖析分析不断调优到5分钟跑完,哈,就像买了辆QQ,你把它魔改成了起步三秒的超跑。 【TODO:现在,我们知道perf是Linux平台上一个轻量的,分析性能问题的利器,它相比其他工具……如果你平时……,那么掌握好工具是……】
250 |
251 | 我这里也有一个小问题想留给你:在这节课中,我使用perf record采样,perf使用的默认计数器是“cpu-clock”, 那么在你的环境中,默认计数器会是什么呢?为什么是这个计数器呢?
252 |
253 |
--------------------------------------------------------------------------------
/perf/pou-xi-de-yuan-li.md:
--------------------------------------------------------------------------------
1 | # 剖析的原理
2 |
3 | 你好,我是hugulas。
4 |
5 | 在上一篇中,我带着你,一起用perf找到了求质数小程序中的性能瓶颈。在寻找性能瓶颈的过程中,我们不需要对应用程序结构有太多的了解,也不需要修改任何应用代码;仅仅是通过perf做了一次剖析(Profiling),程序的性能瓶颈就自然暴露在我们面前。
6 |
7 | 但是,有了剖析工具,知道怎么使用就够了么?~~有没有感觉很神奇呢?我想你一定很好奇,剖析工具是怎么做到这一点的?~~
8 |
9 | ~~今天,我们就来讲一讲剖析的原理。首先我们来聊聊,剖析是什么(Profiling),剖析的原理是什么样的,并且我会用一个具体的例子,帮助你更好地理解。~~
10 |
11 | ~~那么,我为什么要给你讲原理呢?~~
12 |
13 | 如果你尝试着去读一下perf或者OProfile的手册,你会发现每句话你都能看懂,但是实战中却不知道这些选项应该怎么用,产生的报告应该怎么解读。我当年刚入行时,也有过这样的困惑。我曾经问过组里其他的开发者:“我们自己写的工具,我看了文档都不知道怎么用,用户怎么会用?”
14 |
15 | 经过一段时间的工作,我找到了答案,这是因为性能剖析工具和其他软件工具的用户群不同,它的用户是资深程序员和性能专家。它是为这些大师们量身打造的屠龙刀。大师们用剖析工具可以在复杂系统中庖丁解牛般地快速找到性能瓶颈。~~但是初学者想要用好这把屠龙刀却并不容易,因为使用说明也是为这些大师们写的,很多~~
16 |
17 | 但是初学者想要用好这把屠龙刀,却并不容易。你只有了解它的原理,才能合理地使用它去采样,怎么选择采样频率,采样事件,怎么解决各种异常;只有了解它的原理,才能正确地解读它采样得到的结果。
18 |
19 | 今天,我们就来讲一讲剖析的原理。首先我们来聊聊,剖析是什么(Profiling),剖析的原理是什么样的,并且我会用一个具体的例子,帮助你更好地理解。
20 |
21 | 在未来的应用性能分析过程中,你必然会在复杂环境下遇到千奇百怪的问题吗,希望这些原理可以帮助你找到解决问题的线索。
22 |
23 | ## 什么是剖析(Profiling)
24 |
25 | 剖析,我们简单地理解,就是给要分析的对象拍照,通过照片帮助我们理解和分析对象的特点,形象一点说,比如我们去医院拍X光片,拍CT就是剖析的过程。
26 |
27 | 而在计算机领域,**剖析(Profiling)是指\*\***:**通过对系统状态固定频率的采样,并统计分析采样的结果。**【TODO:这里需要加一下剖析/采样又分为剖析法和追踪法两种】\*\*定期进行采样是剖析法(Profiling)和追踪法(Tracing)最大的不同。【TODO:剖析法是指……追踪法是指……】和追踪法相比,定期采样可以有效地减少性能数据采集带来的性能开销。
28 |
29 | 在磁盘IO或者网络领域,剖析采样可以帮助性能分析师,用比较低的性能影响获得粗粒度的性能报告。
30 |
31 | 处理器利用率分析是剖析法最常用、最有效的领域。剖析工具可以通过归纳总结被采集到的指令和调用栈栈信息(stack trace),帮助分析师了解到:哪些代码在消耗处理器资源。
32 |
33 | ## 剖析工具的工作原理是什么样的?
34 |
35 | 【TODO:说到剖析工具,那么我们工作中……,所以这里我主要以perf和OProfile工具为例,来说……】
36 |
37 | perf或者OProfile等工具在进行采样时,每隔一个固定的时间间隙,采集系统上软硬件性能事件,获得不同性能事件的统计结果。
38 |
39 | 它们会记下来在这个采样间隔中执行了多少指令,发生了多少次CacheMiss等等。除了采集软硬件性能事件外,**还会采集当前正在执行的指令地址和进程编号\*\***(**PID**)**\*\*,**我们在这个采样间隙中,采集到的软硬件性能事件都会记到这条指令身上。
40 |
41 | 剖析工具首先找到指令地址和进程编号,通过地址到命名的转换过程,找到指令具体属于哪条进程(命令),哪个线程,哪个模块,哪个函数,甚至哪个代码行。
42 |
43 | 最后,剖析工具会统计每个进程/线程,每个模块,每个函数,每个代码行,每条指令有多少次采样和多少软硬件事件。性能分析师通过这些信息可以顺藤摸瓜,找到有性能问题的模块或者代码。
44 |
45 | 听起来比较抽象,我们不妨一起来看个具体的例子。
46 |
47 | 在上一篇中,我们使用了“perf record -F 999 ./sample 2000”这样一条命令来进行采样。我指定的采样频率是999HZ,也就是每秒999次采样。
48 |
49 | 我们每次采样时,都会采集到当前正在执行的指令的指令地址和进程编号,以及相应的事件计数器信息。比如某一次采样采集到了地址为0x004005c4的指令,它的进程编号是22258。剖析工具就需要对它进行指令地址到命名的转换,以便定位出这条指令属于哪条进程。这里的具体过程如下所示。
50 |
51 | 第一步,**剖析工具通过进程编号\*\***(PID)**22258,就可以**从进程列表获知进程对应的是./sample命令**。** \*\*通过进程编号,找到进程对应命令的办法多种多样,不同平台上不同工具用的方法不同。 比如在Linux系统/proc目录下,每个PID对应了一个目录,打开/proc//cmdline文件, 我们就能找到PID对应的命令行。
52 |
53 | ```text
54 | [hugulas@hugulas sample1]$ cat /proc/22258/cmdline
55 | ./sample 100000
56 | ```
57 |
58 | 第二步, **剖析工具在22258的\*\***地址空间**\*\*中,找到地址0x004005c4对应的模块**。下面的列表是进程22258的内存映射文件(/proc/22258/maps文件,**列表第一列是内存映射区域的起始地址,最后一列是模块名**), 我们可以看到0x004005c4属于sample模块(/home/hugulas/sample,开始地址00400000,结束地址00401000)。
59 |
60 | **/proc/22258/maps文件:\*\***~~(列表第一列是内存映射区域的起始地址,最后一列是模块名)~~\*\*
61 |
62 | ```text
63 | [hugulas@hugulas sample1]$ cat /proc/22258/maps
64 | 00400000-00401000 r-xp 00000000 fd:03 487 /home/hugulas/sample
65 | 00600000-00601000 r--p 00000000 fd:03 487 /home/hugulas/sample
66 | 00601000-00602000 rw-p 00001000 fd:03 487 /home/hugulas/sample
67 | 01135000-01156000 rw-p 00000000 00:00 0 [heap]
68 | 7fc686e19000-7fc686fdb000 r-xp 00000000 fd:00 33645705 /usr/lib64/libc-2.17.so
69 | 7fc686fdb000-7fc6871db000 ---p 001c2000 fd:00 33645705 /usr/lib64/libc-2.17.so
70 | 7fff7fe71000-7fff7fe92000 rw-p 00000000 00:00 0 [stack]
71 | 7fff7fff9000-7fff7fffb000 r-xp 00000000 00:00 0 [vdso]
72 | ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
73 | ```
74 |
75 | 第三步,通过用不同的工具进一步扫描内存映射区域,或者分析二进制模块文件(/home/hugulas/sample), 可以得到sample模块中各函数的地址范围。下面的列表就是sample模块各函数的地址范围。**从各函数的地址范围,我们可以知道0x004005c4属于wasteTime函数(5ad-5db)\*\***。\*\*
76 |
77 | **sample模块中各函数的地址范围:**
78 |
79 | ```text
80 | /home/hugulas/sample
81 | 438 4c0 g _init
82 | 470 480 g printf@plt 480 490 g __libc_start_main@plt
83 | 490 4a0 g malloc@plt
84 | 4a0 4b0 g atoi@plt
85 | 4c0 4f0 g _start
86 | 4f0 520 l deregister_tm_clones
87 | 520 560 l register_tm_clones
88 | 560 580 l __do_global_dtors_aux
89 | 580 5ad l frame_dummy
90 | 5ad 5db g wasteTime
91 | 5db 6d0 g number
92 | 6d0 771 g main
93 | 780 7e5 g __libc_csu_init
94 | 7f0 7f2 g __libc_csu_fini
95 | 7f4 2000 g _fini
96 | ```
97 |
98 | 第四步,**通过sample模块中的debug信息或者额外的debug模块,剖析工具可以把指令地址和C的源代码相关联。**perf可以通过objdump从sample模块中抽取出源代码信息。**指令0x004005c4对应的代码行\*\***是第17行:“testResult += i;”**\*\*。**
99 |
100 | **objdump的输出:**(命令:objdump --source --line-numbers sample\_with\_source)
101 |
102 | ```text
103 | 00000000004005ad :
104 | wasteTime():
105 | /home/hugulas/code/perftools-intro/linux-perf/sample1/sample.c:12
106 | int count;
107 | struct PrimeNumberNode *next;
108 | };
109 |
110 |
111 | int wasteTime(int factor)
112 | {
113 | 4005ad: 55 push %rbp
114 | 4005ae: 48 89 e5 mov %rsp,%rbp
115 | 4005b1: 89 7d ec mov %edi,-0x14(%rbp)
116 | /home/hugulas/code/perftools-intro/linux-perf/sample1/sample.c:13
117 | int i = 0;
118 | 4005b4: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
119 | /home/hugulas/code/perftools-intro/linux-perf/sample1/sample.c:15
120 | int testResult;
121 | for (i = 0; i < factor; i++)
122 | 4005bb: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
123 | 4005c2: eb 0a jmp 4005ce
124 | /home/hugulas/code/perftools-intro/linux-perf/sample1/sample.c:17 (discriminator 2)
125 | {
126 | testResult += i;
127 | 4005c4: 8b 45 fc mov -0x4(%rbp),%eax
128 | 4005c7: 01 45 f8 add %eax,-0x8(%rbp)
129 | /home/hugulas/code/perftools-intro/linux-perf/sample1/sample.c:15 (discriminator 2)
130 | for (i = 0; i < factor; i++)
131 | 4005ca: 83 45 fc 01 addl $0x1,-0x4(%rbp)
132 | /home/hugulas/code/perftools-intro/linux-perf/sample1/sample.c:15 (discriminator 1)
133 | 4005ce: 8b 45 fc mov -0x4(%rbp),%eax
134 | 4005d1: 3b 45 ec cmp -0x14(%rbp),%eax
135 | 4005d4: 7c ee jl 4005c4
136 | /home/hugulas/code/perftools-intro/linux-perf/sample1/sample.c:20
137 | testResult = testResult * 1;
138 | }
139 | return testResult;
140 | 4005d6: 8b 45 f8 mov -0x8(%rbp),%eax
141 | /home/hugulas/code/perftools-intro/linux-perf/sample1/sample.c:21
142 | }
143 | 4005d9: 5d pop %rbp
144 | 4005da: c3 retq
145 | ```
146 |
147 | 剖析工具依次处理每一条采样记录,把这些采样统计到进程、线程、模块、函数、代码行、指令行。性能分析师就能通过剖析工具知道采样在不同层面的分布,比如,如以下列表所示,采样大部分集中在waste time和number两个函数上。
148 |
149 | **采样在函数层面的分布:**
150 |
151 | ```text
152 | Samples: 4K of event 'cycles:u', Event count (approx.): 13437837470
153 | Overhead Command Shared Object Symbol
154 | 99.04% sample sample [.] wasteTime ▒
155 | 0.94% sample sample [.] number
156 | 0.02% sample [kernel] [k] 0xffffffff94d75c90
157 | 0.00% sample ld-2.17.so [.] dl_main ▒
158 | 0.00% sample [kernel] [k] 0xffffffff94d6b730
159 | 0.00% sample ld-2.17.so [.] _start
160 | ```
161 |
162 | 从剖析采样的原理可以看到,处理器利用率分析所需要的信息有以下几个来源: 1. 剖析采样; 2. 进程列表; 3. 进程的地址空间映射; 4. 二进制模块文件。
163 |
164 | 这些信息是系统和应用本来就有的,不需要应用改变自身的行为或者逻辑来提供,这也是剖析分析的一个优势。
165 |
166 | 根据剖析采样的原理,我们在分析和采样时有以下几点需要注意。
167 |
168 | 1. **剖析的结果是采样统计归纳的结果,只具有统计学上的意义。**只有当函数或者指令采样数量足够大的时候,不同函数和指令上的采样数量比较才有意义。比如指令A有2个采样,而指令B有1个采样,并不意味着指令A就一定比指令B花的时间更多。但是如果指令A有2000个采样,而指令B有1000个采样,我们可以认为指令A就一定比指令B花的处理器时间更多。建议:通过控制采样时间和采样频率,保证要分析的关键的函数和指令至少有两位数以上的采样。
169 | 2. **采样频率除了会影想到剖析的精度,也会增加剖析的性能开销和剖析的处理时间**。【TODO:因为……】采样频率越高,剖析的精度越高,但是性能开销也会越大,极端情况有可能下会干扰到应用的行为特征, 同时采样文件也越大。反之,采样频率越小,虽然性能开销变小,对系统的干扰也变小,但是采样的精度也相应下降。建议:如果是第一次对应用或者系统采样,可以尝试不同采样频率,并和不采样的性能做比较,选择性能开销可以接受的采样频率。
170 | 3. **剖析工具把采样进行函数和代码级别的归纳时,需要模块文件提供符号表和debug信息。**因此,我建议你尽量不要通过strip命令砍掉模块的符号表信息,这是很多人在XX情况下会执行的操作。对于要进行代码级分析的模块,可以通过重新编译提供debug信息。
171 |
172 | **总结**
173 |
174 | 【TODO这里还是总结一下今天讲的内容哈。加强用户的记忆】
175 |
176 | 在介绍完剖析的原理后,我有一个小问题留给你:如果我们要把perf.data文件从服务器拷贝到笔记本上来分析,需要注意什么问题?
177 |
178 |
--------------------------------------------------------------------------------
/performance-arsenal.md:
--------------------------------------------------------------------------------
1 | # 性能分析的军火库
2 |
3 | 你好,我是陈沁悦。
4 |
5 | 小时候,我喜欢看各种警匪片,枪战片,动作片和谍战片。施瓦辛格,汤姆·克鲁斯,007和成龙在枪炮声中伴我度过了愉快的童年。
6 |
7 | 这些动作片中经常有这样一个场景,行动之前,主角会被带到一个隐蔽的军火库,墙上或者车里挂满了各种武器,手枪、步枪、手榴弹,火箭筒应有尽有。
8 |
9 | 007还会拿到一些特殊的高科技武器,而黑衣人则会有对付外星人的特殊装备。这个场景非常有代入感,小时候的我看到这个场景总是不免对着屏幕流口水,是选那把有六个炮筒的加特林还是异常威武的火箭筒呢?(你如果感兴趣,可以去B站,看看这些让人激动的片段。([https://www.bilibili.com/video/av49613050/](https://www.bilibili.com/video/av49613050/))
10 |
11 | 每个职业都有自己的军火库。性能分析师不例外,我想,作为开发者的你可能会好奇,我不是性能分析师,需要这样的军火库吗?
12 |
13 | 我曾经给服务团队的同事介绍过我们开发的性能分析工具。介绍完后,我问服务团队的同事:这些能帮你解决客户那边的问题吗?
14 |
15 | 服务团队的同事告诉我,大家都非常乐意储备这样的工具知识。因为一旦当你遇到合适的问题时,你就不再是赤手空拳,而是手上握着重机枪,所向披靡。
16 |
17 | 我们这一篇将讲一讲性能分析师的军火库。作为应用开发者,我们只需要熟练掌握其中的两三种常用的工具。对于其余的,我们需要知道它们能分析什么样的性能问题。这样我们就能在遇到性能问题时迅速找到趁手的武器,而不是赤手空拳去现场插桩写计数器了。
18 |
19 | ## 性能分析工具有哪些?
20 |
21 | 我们来看下面这张思维导图,这张图上满满的工具就是性能分析师在Linux平台上的军火库了。我按照性能分析的对象和分析手段不同,对这些武器做了分类。这样,你在遇到性能问题的时侯,通过这张图,按图索骥就能找到自己能用的工具,然后看工具的文档现学现卖,也能慢慢找到应用中性能问题。
22 |
23 | 
24 |
25 | 下面,我来讲一讲,我们拿到这张图,应该怎么用?
26 |
27 | **这张图的左侧\*\***,我**\*\*把工具按照性能分析的对象进行分类的,包括了处理器,内存,网络,I/O,图形处理器,编程语言和中间件几大块。**
28 |
29 | **图的右侧是把工具按照分析手段不同进行分类,分\*\***为概要分析工具(overview tool),采样工具(sampling),追踪工具(trace)和可视化集成环境几部分。\*\*
30 |
31 | 每一类中的工具相互都有功能重叠,甚至是可以互相取代,比如sar,nmon和top,再比如perf和OProfile。
32 |
33 | 这是因为我们要去做性能分析的平台,很有可能并不是我们的开发环境,有可能是测试环境,有可能是客户的生产环境。我们并不一定能安装自己熟悉的工具,而只能有什么用什么。
34 |
35 | ## 如何利用你的性能分析军火库
36 |
37 | 在遇到性能问题时,我们应该先通过概要分析工具采集信息。 通过概要分析,我们可以判断存不存在一些明显的问题根源(比如内存/磁盘耗尽),并且定位出问题属于哪个领域,这是一个处理器问题呢,还是I/O问题?
38 |
39 | 如果**你是运维**,那么你很有可能根据问题所属的领域,在图的左侧寻找相应领域的性能分析工具。处理器不够加处理器,paging太多买内存。
40 |
41 | 但是,如果**你是应用开发者**,你有机会去调优应用代码,对吧?那么我们的思路就会和运维有所区别。我建议你从采样工具和映描工具开始。我们在这里要先做一个判断。这是一个处理器利用率有关的问题吗?
42 |
43 | ### **“处理器利用率低”相关问题**
44 |
45 | **如果这是一个和处理器利用率有关的问题**(处理器利用率高),并且很有可能和我们的应用有关(比如我们应用的进程CPU占用率特别高),我们应该优先考虑使用采样工具(oprofile/perf)寻找出有问题的模块,函数和代码。
46 |
47 | 因为这是效率高而且额外负载比较小的手段。当我们找到有问题的模块,函数和代码后,我们可以通过性能调优来解决这个问题。
48 |
49 | 但是,有的时候,我们会遇到另外一个问题,处理器占用率最高的函数本来就是计算密集型的,比如矩阵乘法函数,调得多了自然就消耗处理器资源。这时,我们就需要通过采样工具的调用图,或者通过映描工具(trace)比如gprof从trace中找到是谁调用了它,看看有没有优化空间。
50 |
51 | ### **“应用慢”相关问题**
52 |
53 | **如果这并不是一个处理器利用率有关的问题,而仅仅是应用慢的问题。**我们就需要通过映描工具比如iTrace、strace、Gprof根据调用关系去一层层的从顶向下寻找到底哪一部分模块,哪一部分函数是慢的根源。
54 |
55 | 要注意的是,细粒度的映描工具比如Gprof可能带来比较大的额外开销,而让应用的行为发生比较大的变化,比如某些频繁调用的函数会消耗更多的时间,这会给我们的分析带来困难。
56 |
57 | 另外,通过映描工具做分析往往需要更多时间和对工具更多的了解。当我们找到是哪一段代码慢时,我们可以尽可能地去调优它。左侧的工具这时也能帮助我们从另一个维度理解这段代码慢的根源。
58 |
59 | ### 编程语言和中间件相关问题
60 |
61 | 当性能问题可能和具体的编程语言环境有关时,比如我们的应用是Java,Python,JavaScript这样的解释执行语言编写的,**这些语言环境和二进制码应用并不一样,我们就要考虑使用这些语言环境相关的性能分析工具。\*\***比如\*\*
62 |
63 | 而当性能问题被定位到我们依赖的软件和中间件时,比如MySQL数据库时,我们需要使用和这些中间件相关的性能分析工具。中间件相关的性能分析工具实在太多了,我在这里就不一一赘述了。
64 |
65 | ### 可视化集成开发环境
66 |
67 | 如果你可以在自己的开发环境中重现这个性能问题,我强烈建议你使用**可视化集成开发环境。**
68 |
69 | 通过可视化集成开发环境,你可以快速完成采样到分析的每一个步骤,可以通过可视化界面从不同的视图分析性能问题。
70 |
71 | 针对不同的目标平台,有不同的集成开发环境。如果是x86平台,可以选择Intel VTune客户端或者集成到Visual Studio中的Intel Vtune,如果是IBM系的Power,Z,i平台,可以选择Rational Developer。如果是GPU,还可以试试AMD的Code XL。
72 |
73 | 如果你追求武器库般的视觉冲击力,我自己非常喜欢链接中Brendan D. Gregg画的这张图,推荐Linux平台上的应用开发者收藏。([http://www.brendangregg.com/Perf/linux\_perf\_tools\_full.svg](http://www.brendangregg.com/Perf/linux_perf_tools_full.svg))在他的图中,工具和Linux的不同模块绑在一起,看起来更像装满了各种枪炮的武器架。
74 |
75 | 下面这张图是我画的性能分析师军火库的全平台版本。作为开发者,我们在工作生涯中会为不同的平台编写应用。
76 |
77 | 
78 |
79 | 它可以是历史悠久的主机上运行的银行核心业务,也可以是在Windows和安卓平台上运行的社交软件。但是没有关系,我们只要掌握了某个平台上的一种工具,根据它的名称和用途,我们就能在其他平台上找到替代品。
80 |
81 | 对于Unix平台来说,大部分的工具和Linux平台上名称都是一致的,或者稍微改改名字,比如Linux上的top和AIX上的topas。 微软的Windows平台和IBM的主机zOS系统会有点特殊。这两个操作系统都是单一供应商,Windows平台上很多性能分析功能都可以在WPR和WPA中找到,而zOS平台上很多性能分析数据都可以在RMF和SMF中获得。
82 |
83 | 在手机平台上,安卓因为是从Linux派生而来,很多Linux上的性能分析工具比如oprofile也能在安卓上使用,谷歌也为安卓系统提供了一些专有的性能分析工具。
84 |
85 | ## 总结
86 |
87 | 最后,我好像遗忘了某个用户数特别巨大的平台,在这个平台上如果要采集采样和trace,应该用哪个工具呢?
88 |
89 |
--------------------------------------------------------------------------------