UP | HOME

性能分析

目录

google-perftools

介绍

性能分析通过抽样方法完成,默认是 1 秒 100 个样本,一个样本是 10 毫秒,即时间单位是 10 毫秒;可以通过环境变量 CPUPROFILE_FREQUENCY 设置采样频率。

CPU profiler 是基于采样工作的。所以采样次数影响着性能报告的准确性。如果采样次数过少,则你会发现同样的程序同样的数据,每次输出的性能报告中的热点都不一样。

使用方法

直接调用 API

在代码中调用 ProfilerStartProfilerStop 这两个函数。

链接静态库

在代码 link 过程中添加参数 –lprofiler

gcc […] -o helloworld –lprofiler

运行程序: CPUPROFILE=./helloworld.prof ./helloworld

指定要 profile 的程序为 helloworld,并且指定产生的分析结果文件的路径为./helloworld.prof

链接动态库

这种方式和静态库的方式差不多,但不用重新编译或重新链接,所以使用非常方便。只需要在运行时添加 LD_PRELOAD 变量。

LD_PRELOAD="/usr/lib/libprofiler.so" CPUPROFILE=./helloworld.prof ./helloworld

使用信号触发 profile

手动设置信号

由于我们的程序有可能是服务程序,而服务程序不会自动执行完退出,如果以 ctrl+c 退出也不是正常的 exit(0) 的方式退出,而这会导致我们在 profile 的时候,收集到的数据不全甚至是空的,采用如下解决办法:

ProfilerStartProfilerStop 这 2 个函数封装到两个信号处理函数中,给服务程序发信号 SIGUSR1,就开始 profile,给服务程序发信号 SIGUSR2,就停止 profile。这样我们可以随时对程序进行 profiling,并获得数据。代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <google/profiler.h>

//SIGUSR1: start profiling
//SIGUSR2: stop profiling

static void gprof_callback(int signum)
{
  if (signum == SIGUSR1)
    {
      printf("Catch the signal ProfilerStart\n");
      ProfilerStart("bs.prof");
    }
  else if (signum == SIGUSR2)
    {
      printf("Catch the signal ProfilerStop\n");
      ProfilerStop();
    }
}

static void setup_signal()
{
  struct sigaction profstat;
  profstat.sa_handler = gprof_callback;
  profstat.sa_flags = 0;
  sigemptyset(&profstat.sa_mask);
  sigaddset(&profstat.sa_mask, SIGUSR1);
  sigaddset(&profstat.sa_mask, SIGUSR2);

  if ( sigaction(SIGUSR1, &profstat,NULL) < 0 )
    {
      fprintf(stderr, "Fail to connect signal SIGUSR1 with start profiling");
    }
  if ( sigaction(SIGUSR2, &profstat,NULL) < 0 )
    {
      fprintf(stderr, "Fail to connect signal SIGUSR2 with stop profiling");
    }
}

int main(int argc,char** argv)
{
  //
  // ...
  //

  setup_signal();

  //
  // ...
  //
 
  return 0;
}

启动程序后,可以采用 kill -s SIGUSR1 <pid> 和 kill -s SIGUSR2 <pid> 来开始采样和停止采样。

使用 CPUPROFILESIGNAL 设置信号

设置的信号必须是正常情况下未被程序使用的。

CPUPROFILE=./helloworld.prof CPUPROFILESIGNAL=12 ./helloworld

可以使用 killall -12 hellworld 触发 profile 开始,然后过一段时间后再次使用 killall -12 hellworld 触发 profile 结束并生成文件。

数据分析

生成数据

  1. 生成文字数据 google-pprof <bin path> <profile path> --text
  2. 生成网页数据 google-pprof <bin path> <profile path> --web

数据含义

每一列的数据解释:

  1. 该函数中的采样数量
  2. 该函数中的采样百分比
  3. 到目前为止打印的函数中的采样百分比
  4. 该函数和被其调用的函数中的采样数量
  5. 该函数和被其调用的函数中的采样百分比

gprof

介绍

grof 可以获取 C 程序运行期间的统计数据,如每个函数的耗时,调用次数及各个函数的调用关系,(gprof 统计的是 CPU 占用时间,I/O 时间不计算在内。通常 gprof 的采样周期是 0.01s,统计项越接近这个值误差可能越大。若函数的运行时间低于 0.01S,统计值会显示为 0。

类似于 gdb,gprof 需要对待分析的程序做一些改动,因此在程序编译的时候需要加上"-pg"选项,如果程序的某个模块在编译的时候没有加上"-pg",则该模块的函数会被排除在统计范围之外。比如想要查看库函数的 profiling,则需在链接库函数的时候用“-lc_p"代替”-lc"(gprof 是各个类 UNIX 的标准工具,系统自带的链接库通常有两个版本,它们的区别在于编译的时候是否加上了"-pg"。用-lc_p 等于告诉编译器选择加上了"-pg"的那个版本)。 加上"-pg"选项后,程序的入口会于 main()之前调用 monstartup(),主要是申请内存存储接下来获取的统计信息。

在每个函数中会调用_mcount(),主要是在函数的堆栈中查询父函数和子函数的地址并保存下来。最后会在程序退出前调用_mcleanup(),将统计结果保存到 gmon.out 中,并完成清除工作。

gprof 统计各个函数的运行时间是采用的抽样的方法,周期性的查看 Programcounter 指向哪一个函数的地址段,并把结果以直方图的形式保存下来。

使用方法

  1. 编译时添加选项 -pg
  2. 运行程序,生成 gmonf.out 文件
  3. 使用 gprof 进行分析, gprof <bin path> <gmonf path>

数据分析

Flat Profile 示例图

标注 释义
%time 每个函数占用的时间比例,所有函数占比和为 100%
cumulative seconds 函数及其调用函数执行累计占用时间
self seconds 单独函数执行累计占用时间
calls 函数调用次数
self ms/call 每次调用函数花费的时间,单位毫秒, 不包含调用函数运行的时间
total ms/call 每次调用函数花费的时间,单位毫秒,包括调用函数运行的时间
name 函数名称

Call Graph 示例图

标注 释义
index 每个函数第一次出现时分配一个编号,根据编号可以方便查找函数的具体分析数据
%time 函数以及调用子函数所占用的总运行时间的百分比
self 函数的总运行时间
children 子函数执行的总时间
called 函数被调用的次数,不包括递归调用
name 函数名称, name 列中,可查看函数之间的调用关系

作者: Petrus.Z

Created: 2021-09-01 Wed 00:39