1. 程式人生 > >Profile 工具系列之二: gperftools

Profile 工具系列之二: gperftools

簡介

  • gperftools 原名 Google Performance Tools,是一套可以做 profile 的工具,由 google 提供。
  • 目前看來在 Linux 上特別的好使,Windows 基本不能用(官網說 Windows 正在開發,不知道會不會有下文)
  • CPU profile 只是這個工具其中一個 feature,我們就用這個 feature。
  • 其他 feature 還有 tcmalloc:一個比malloc更快的記憶體管理演算法;還有 heap profiler,heap checker。

一. 安裝

  • clone 程式碼:
git clone https://github.com/gperftools/gperftools.git
  • 根據 INSTALL 裡面的介紹,首先需要安裝 autoconf,automake,libtool。然後才能產生 configure 指令碼,所以安裝:
yum install autoconf
yum install automake
yum install libtool
  • 產生 configure 指令碼:
/autogen.sh
  • 根據 INSTALL 裡面的介紹,在 64 Linux 位系統上,建議先裝 libunwind 然後再 config 和 make,所以安裝:
tar -xzvf libunwind-0.99-beta.tar.gz
cd libunwind-
0.99-beta ./configure --prefix=path/to/where/you/want make make install
  • 設定 configure 需要的環境變數:我們安裝了 libunwind 之後,需要設定幾個環境變數,這樣做 configure gperftools 的時候,才能找到我們裝的 libunwind:
export  CPPFLAGS=-I/path/to/libunwind_install/include
export  LDFLAGS=-L/path/to/libunwind_install/lib
  • 執行 configure 指令碼,只需要用 –prefix 指定安裝到哪裡即可:
./configure --prefix=/path/to/gperftools_install
make
make install

二. 使用

例1. HelloWorld

  • 一個helloworld原始檔:hello.c
/* hello.c */
#include<stdio.h>
int main(int argc,char** argv){
    printf("hello world\n");
}
  • 編譯時加入 -lprofiler 選項:
gcc -o hello hello.c -lprofiler -L/path/to/gperftools_install/lib
  • 執行之前設定環境變數:
export CPUPROFILE=hello.prof.out
  • 執行之前把庫檔案拷貝到系統庫目錄,要不然執行時找不到 libprofiler 庫:
cp path/to/gperftools_install/lib/* /usr/lib64/
  • 執行:
./hello
  • 執行結束後生成了 hello.prof.out
  • 分析該檔案
    • 首先把 pprof 拷貝到 /usr/bin

      cp path/to/gperftools_install/bin/pprof /usr/bin/
    • 然後用 pprof 分析剛才生成的 hello.prof.out

      pprof --text ./hello ./hello.prof.out
  • 輸出了兩行沒什麼意義的東西:

    Using local file ./hello.
    Using local file ./hello.prof.out.

例2. 稍複雜的程式

  • 準備一個原始檔 take_time.c 如下:
/* take_time.c */
#include<stdio.h>

int take()
{
    int n = 0;
    int i = 0, j = 0;
    for(i = 0; i < 1000000; i++)
        for(j = 0; j < 10000; j++)
        {
            n |= i%100 + j/100;
        }
    return n;
}

int main(int argc,char** argv)
{
    int r =  take();
    printf("%d\n",r);
    return 1;
}
  • 編譯,設定環境變數,執行都同上,就不重寫了
  • 分析輸出檔案:
pprof --text take take.prof.out
  • 得到如下結果:
Using local file take.
Using local file take.prof.out.
Removing main from all stack traces.
Removing __libc_start_main from all stack traces.
Removing _start from all stack traces.
Total: 2937 samples
    2937 100.0% 100.0%     2937 100.0% take
  • 可以看到,正確的統計出了函式 take 佔用了 100.0% 的時間

例3. 共享庫

  • 共享庫的例子主要為了說明以下幾點:

    1. 在編譯 xxxxxx.so 時,不用加上 -lprofiler,只要在把 xxxxxx.so 連線進主程式時加上即可。庫裡的函式一樣可以被統計到。
    2. 即使 xxxxxx.so 裡有些函式沒有被主程式直接使用(該庫的標頭檔案沒有暴露該函式,這些函式是庫自用的),即主程式的符號表里根本不存在該函式,該函式也能被成功統計到資訊。本例中的 worker() 函式就將扮演這個角色。
  • 準備3個原始檔,庫:libtake.c,主程式:main.c

/* libtake.h */
int take_interface();

/* lilbtake.c */
#include "libtake.h"    
int worker()
{
    int n = 0;
    int i = 0, j = 0;
    for(i = 0; i < 1000000; i++)
        for(j = 0; j < 10000; j++)
        {
            n |= i%100 + j/100;
        }
    return n;
}

int take_interface()
{
    int n = 0;
    int i = 0, j = 0;
    for(i = 0; i < 100000; i++)
        for(j = 0; j < 10000; j++)
        {
            n |= i%100 + j/100;
        }
    int r = worker();
    return n+r;
}

/* main.c */
#include <stdio.h>
#include "libtake.h"
int main(int argc,char** argv)
{
    int r =  take_interface();
    printf("%d\n",r);
    return 1;
}
  • 編譯生成庫,注意,不用加 -lprofiler:
gcc --shared  -fPIC -o libtake.so  libtake.c
  • 編譯生成主程式,注意,此時加入 -lprofiler:
gcc -o main main.c -lprofiler -L/home/zsl/gperftools_build/lib -ltake -L./
  • 為了能執行main,把 libtake.so 拷貝到 /usr/lib/,並進行 ldconfig:
cp libtake.so /usr/lib/
ldconfig
  • 設定環境變數並執行:
export CPUPROFILE=main.prof.out
./main
  • 分析結果:
pprof --text main main.prof.out 
  • 輸出結果如下:
Total: 3228 samples
    2935  90.9%  90.9%     2935  90.9% worker
     293   9.1% 100.0%     3228 100.0% take_interface
       0   0.0% 100.0%     3228 100.0% __libc_start_main
       0   0.0% 100.0%     3228 100.0% _start
       0   0.0% 100.0%     3228 100.0% main
  • 可以看到,函式 worker 佔用了 90.9% 的時間,函式 take_interface 自身佔用了 9.1% 的時間,而 take_interface 加上它的子函式(即 worker)一共佔用了 100.0% 的時間。結果是符合預期的。

  • 注意:

    • 我們編譯 libtake.so 的時候並沒有加 -lprofiler(並不像 gprof 要在編譯庫的時候也加上 -pg)
    • main.c 並不知道有 worker() 函式的存在,nm main | grep worker也輸出空
    • 但我們確實抓到了 worker 函式消耗的時間

例4. 動態載入(dlopen)的共享庫

  • 例3中我們使用共享庫的方法是在編譯主程式時,把共享庫 libtake.so 連到了主程式上。
  • 而實際開發過程中,尤其是開發外掛時經常使用的,是執行時載入動態庫,即使用 dlopen 的方式。
  • 原始檔:libtake.c 和 libtake.h 不變,甚至可以直接使用例3裡編譯好的庫。main.c 改造成如下形式(改名為 main_dl.c):
#include<stdio.h>
#include<dlfcn.h>

char LIBPATH[] = "./libtake.so";
typedef int (*op_t) ();

int main(int argc, char** argv)
{
    void* dl_handle;
    op_t take_interface;
    dl_handle = dlopen(LIBPATH, RTLD_LAZY);
    take_interface = (op_t)dlsym(dl_handle, "take_interface");
    printf("%d\n", (take_interface)() );
    dlclose(dl_handle);
    return 0;
}
  • 編譯主程式,加入 -lprofiler,注意,這次不用再 -ltake 去連線自己的庫了,不過要加上 -ldl 因為使用了 dlopen:
gcc -o main_dl main_dl.c -lprofiler -L/home/zsl/gperftools_build/lib -ldl
  • 設定環境變數和執行不再重複,分析出的結果如下(省略部分):
Total: 3230 samples
     447  13.8%  13.8%      447  13.8% 0x00007f53418a65e7
     278   8.6%  22.4%      278   8.6% 0x00007f53418a65d3
       .....
       0   0.0% 100.0%     2936  90.9% 0x00007f53418a6699
       0   0.0% 100.0%     3230 100.0% __libc_start_main
       0   0.0% 100.0%     3230 100.0% _start
       0   0.0% 100.0%     3230 100.0% main
  • 我們發現它找不到庫 libtake.so 裡的符號了,全都用記憶體地址代替了。
  • 這個問題我們只需要把 dlclose() 去掉即可。去掉後重編 main_dl,分析結果正常。

例5. 非常複雜:ffmpeg

  • 我們現在用 gperftools 來對複雜的專案 ffmpeg 進行 profile
  • 下載 ffmpeg 最新程式碼,configure 的時候加上如下選項,以使其編譯時能連線 -lprofiler:
--extra-ldflags=-lprofiler --extra-ldflags=-L/home/zsl/gperftools_build/lib
  • 為了以防萬一,我還加上了 --disable-stripping

  • 其他配置選項根據個人需求,我的完整配置命令如下:

./configure --prefix=/home/zsl/ffmpeg_build --enable-shared --disable-static \
--disable-stripping --disable-avdevice --disable-yasm \  
--extra-ldflags=-lprofiler --extra-ldflags=-L/home/zsl/gperftools_build/lib 
  • make && make install
make -j 16
make install
  • 因為是共享庫,執行 ffmpeg 之前把lib裡的東西都拷貝到 /usr/lib
  • 設定 gperftools 需要的環境變數:
export CPUPROFILE=ffmpeg.prof.out
  • 執行 ffmpeg,得到轉碼結果的同時得到效能統計檔案 ffmpeg.prof.out
./ffmpeg -i /home/zsl/contents/input.mp4 output.mp4
  • 分析效能:
pprof --text ffmpeg ffmpeg.prof.out  > prof_mp4.txt

三. 圖形化輸出

  • 用 –gv 取代 –text 即可
  • 用 –pdf 可以直接把圖形結果生成 pdf 檔案:
pprof --pdf ffmpeg ffmpeg.prof.out  > prof_mp4.pdf

四. 問題

1. x64系統的崩潰問題

  • 使用過程中,即設好了環境變數然後執行 ffmpeg 轉碼的過程中,會經常的崩潰,報段錯誤(Segmentation fault)。官方文件也說了,在x64的系統上確實有這問題。
  • 這種崩潰是隨機發生的,多試幾次,總有不崩的時候。
  • 官方文件建議的 workarounds:
  • 用 ProfilerStart()/ProfilerStop() 包裹需要 profile 的程式碼段,而不是使用環境變數 CPUPROFILE 進行全部程式碼的 profile。
  • 這個方法對我不適用。
  • 官方文件裡關於這個問題的描述如下:

64-BIT ISSUES

2) On x86-64 64-bit systems, while tcmalloc itself works fine, the
cpu-profiler tool is unreliable: it will sometimes work, but sometimes
cause a segfault. I’ll explain the problem first, and then some
workarounds.

Note that this only affects the cpu-profiler, which is a
gperftools feature you must turn on manually by setting the
CPUPROFILE environment variable. If you do not turn on cpu-profiling,
you shouldn’t see any crashes due to perftools.

The gory details: The underlying problem is in the backtrace()
function, which is a built-in function in libc.
Backtracing is fairly straightforward in the normal case, but can run
into problems when having to backtrace across a signal frame.
Unfortunately, the cpu-profiler uses signals in order to register a
profiling event, so every backtrace that the profiler does crosses a
signal frame.

In our experience, the only time there is trouble is when the signal
fires in the middle of pthread_mutex_lock. pthread_mutex_lock is
called quite a bit from system libraries, particularly at program
startup and when creating a new thread.

The solution: The dwarf debugging format has support for ‘cfi
annotations’, which make it easy to recognize a signal frame. Some OS
distributions, such as Fedora and gentoo 2007.0, already have added
cfi annotations to their libc. A future version of libunwind should
recognize these annotations; these systems should not see any
crashses.

Workarounds: If you see problems with crashes when running the
cpu-profiler, consider inserting ProfilerStart()/ProfilerStop() into
your code, rather than setting CPUPROFILE. This will profile only
those sections of the codebase. Though we haven’t done much testing,
in theory this should reduce the chance of crashes by limiting the
signal generation to only a small part of the codebase. Ideally, you
would not use ProfilerStart()/ProfilerStop() around code that spawns
new threads, or is otherwise likely to cause a call to
pthread_mutex_lock!

解決方法:

  • 更新到最新的 libunwind(1.2rc)即可,下載地址:
  • 下載後重新安裝 libunwind,安裝到新的目錄
  • 然後重新編譯 gperftools,編譯之前設定 CPPFLAGS 和 LDFLAGS 為新的 libunwind 的目錄,編譯好後把新編出來的 gperftools 的庫檔案拷貝到 /usr/lib64 覆蓋原來的。
  • 重新編譯被 profile 的程式,連線新編出來的 gperftools 庫即可