性能优化-发现性能问题

为了取得程序的一丁点性能提升而大幅度增加技术的复杂性和晦涩性能,这个买卖做不得,这不仅仅是因为复杂的代码容器滋生bug,也因为他会使日后的阅读和维护工作要更加艰难。《Unix编程艺术》

为什么要性能优化

也许是想要支持更高的吞吐量,想要更小的延迟,或者提高资源的利用率等,这些都是性能优化的目标之一。不过需要提醒的是,不要过早的进行性能优化。如果当前并没有任何性能问题,又何必耗费这个精力呢?当前一些有助于提高性能的编码习惯还是可以时刻保持的。

目标

全面的性能优化不是一件简单的事情。本系列文章不在于介绍性能优化原理或者特定的算法优化。旨在分享一些实践中常用到的技巧,同时也主要关注CPU方面

如何发现性能瓶颈

解决性能问题的第一步是发现性能问题。如何快速发现性能问题呢?对于本文来说,如何发现那些使CPU不停地瞎忙的代码呢?为什么这里是说让CPU瞎忙的代码?

举个例子,完成某个事情,你可能只需要一个CPU时间片,但是由于代码不够好,使得仍然需要多个CPU时间片。导致CPU非常忙碌,而无法继续提高它的效率。

top

这个命令相信大家都用过,可以实时看到进程的一些状态。它的使用方法有很多文章不厌其烦地对其进行了介绍,本文不打算进行介绍。我们可以通过top命令看到某个进程占用的CPU,但是CPU占用高并不代表它有性能问题,也有可能是CPU正在有效地高速运转,并没有占着茅坑不拉屎。

快速发现

想必我们都听过八二法则,同样的,80%的性能问题集中于20%的代码。因此我们只要找到这20%的部分代码,就可以有效地解决一些性能问题。

本文使用perf命令,它很强大,支持的参数也非常多,不过没关系,本文也没打算全部介绍。

系统中可能没有perf命令,ubuntu可以使用如下方法安装:

1
sudo apt install linux-tools-common

实例

直接来看示例吧。例子很简单,只是将字符串的字母转为大写罢了。当然了,很多人可能一眼就看出了哪里有性能问题,不过没关系,这个例子只是为了说明perf的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//来源:公众号【编程珠玑】
//作者:守望先生
//toUpper.c
#include<stdlib.h>
#include<stdio.h>
#include<time.h>
#include<ctype.h>
#include<string.h>
#include<sys/time.h>
#define MAX_LEN 1024*1024
void printCostTime(struct timeval *start,struct timeval *end)
{
if(NULL == start || NULL == end)
{
return;
}
long cost = (end->tv_sec - start->tv_sec) * 1000 + (end->tv_usec - start->tv_usec)/1000;
printf("cost time: %ld ms\n",cost);
}
int main(void)
{
srand(time(NULL));
int min = 'a';
int max = 'z';
char *str = malloc(MAX_LEN);
//申请失败则退出
if(NULL == str)
{
printf("failed\n");
return -1;
}
unsigned int i = 0;
while(i < MAX_LEN)//生成随机数
{
str[i] = ( rand() % ( max - min ) ) + min;
i++;
}
str[MAX_LEN - 1] = 0;
struct timeval start,end;
gettimeofday(&start,NULL);
for(i = 0;i < strlen(str) ;i++)
{
str[i] = toupper( str[i] );
}
gettimeofday(&end,NULL);
printCostTime(&start,&end);
free(str);
str = NULL;
return 0;
}

编译成可执行程序并运行:

1
2
$ gcc -o toUpper toUpper.c
$ ./toUpper

这个时候我们用top查看结果发现toUpper程序占用CPU 100%:

1
2
3
$ top -p `pidof toUpper`
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
24456 root 20 0 5248 2044 952 R 100.0 0.0 0:07.13 toUpper

打开另外一个终端,执行命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ perf top -p `pidof toUpper`
Samples: 1K of event 'cycles:ppp', Event count (approx.): 657599945
Overhead Shared Object Symbol
99.13% libc-2.23.so [.] strlen
0.19% [kernel] [k] perf_event_task_tick
0.11% [kernel] [k] prepare_exit_to_usermode
0.10% libc-2.23.so [.] toupper
0.09% [kernel] [k] rcu_check_callbacks
0.09% [kernel] [k] reweight_entity
0.09% [kernel] [k] task_tick_fair
0.09% [kernel] [k] native_write_msr
0.09% [kernel] [k] trigger_load_balance
0.00% [kernel] [k] native_apic_mem_write
0.00% [kernel] [k] __perf_event_enable
0.00% [kernel] [k] intel_bts_enable_local

其中pidof命令用于获取指定程序名的进程ID。

看到结果了吗?可以很清楚地看到,strlen函数占用了整个程序99%的CPU,那这个CPU的占用是否可以优化掉呢?我们现在都清楚,显然是可以的,在对每一个字符串进行大写转换时,都进行了字符串长度的计算,显然是没有必要,可以拿到循环之外的。

同时我们也关注到,这里面有很多符号可能完全没见过,不知道什么含义了,比例如reweight_entity,不过我们知道它前面有着kernel字样,因此也就明白,这是内核干的事情,仅此而已。

这里实时查看的方法,当然你也可以保存信息进行查看。

1
$ perf record -e cycles -p `pidof toUpper` -g -a

执行上面的命令一段时间,用于采集相关性能和符号信息,随后ctrl+c中止。默认当前目录下生成perf.data,不过这里面的数据不易阅读,因此执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ perf report
+ 100.00% 0.00% toUpper [unknown] [k] 0x03ee258d4c544155
+ 100.00% 0.00% toUpper libc-2.23.so [.] __libc_start_main
+ 99.72% 99.34% toUpper libc-2.23.so [.] strlen
0.21% 0.02% toUpper [kernel.kallsyms] [k] apic_timer_interrupt
0.19% 0.00% toUpper [kernel.kallsyms] [k] smp_apic_timer_interrupt
0.16% 0.00% toUpper [kernel.kallsyms] [k] ret_from_intr
0.16% 0.00% toUpper [kernel.kallsyms] [k] hrtimer_interrupt
0.16% 0.00% toUpper [kernel.kallsyms] [k] do_IRQ
0.15% 0.15% toUpper libc-2.23.so [.] toupper
0.15% 0.00% toUpper [kernel.kallsyms] [k] handle_irq
0.15% 0.00% toUpper [kernel.kallsyms] [k] handle_edge_irq
0.15% 0.00% toUpper [kernel.kallsyms] [k] handle_irq_event
0.15% 0.00% toUpper [kernel.kallsyms] [k] handle_irq_event_percpu
0.14% 0.00% toUpper [kernel.kallsyms] [k] __handle_irq_event_percpu
0.14% 0.01% toUpper [kernel.kallsyms] [k] __hrtimer_run_queues
0.13% 0.00% toUpper [kernel.kallsyms] [k] _rtl_pci_interrupt

其中-g参数为了保存调用调用链,-a表示保存所有CPU信息。

因此就可以看到采样信息了,怎么样是不是很明显,其中的+部分还可以展开,看到调用链。
例如展开的部分信息如下:

1
2
3
- 100.00% 0.00% toUpper libc-2.23.so [.] __libc_start_main
- __libc_start_main
99.72% strlen

当然了,实际上你也可以将结果重定向到另外一个文件,便于查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ perf report > result
$ more result
# Event count (approx.): 23881569776
#
# Children Self Command Shared Object Symbol
# ........ ........ ....... ................. ..............................
...................
#
100.00% 0.00% toUpper [unknown] [k] 0x03ee258d4c544155
|
---0x3ee258d4c544155
__libc_start_main
|
--99.72%--strlen
100.00% 0.00% toUpper libc-2.23.so [.] __libc_start_main
|
---__libc_start_main
|
--99.72%--strlen
99.72% 99.34% toUpper libc-2.23.so [.] strlen
|
--99.34%--0x3ee258d4c544155

这样看也是非常清晰的。

不过不要高兴地太早,并不是所有情况都能清晰的看到具体问题在哪里的。

至于本文例子的性能问题怎么解决,相信你已经很清楚了,只需要把strlen提到循环外即可,这里不再赘述。

总结

本文的例子过于简单粗暴,但是足够说明perf的使用,快速发现程序中占用CPU较高的部分,至于该部分能否被优化,是否正常就需要进一步分析了。不过别急,后续将会分享一些常见的可优化的性能点。

守望 wechat
关注公众号[编程珠玑]获取更多原创技术文章
出入相友,守望相助!