メモの日々


2010年04月28日(水) [長年日記]

[dev][c++] google-perftoolsでプロファイリング

C++で作ったプログラムのプロファイリングがしたくて、google-perftoolsを使ってみた。バージョンは1.5。GNU gprofでもいいんだけど、GCC以外でも使いたかったので。

使い方

まず使い方を簡単にメモ。google-perftoolsにはいくつか機能があるが、使ったのはCPUプロファイラ機能のみ。

CPUプロファイラはLD_PRELOADを使って利用することもできるが、ドキュメントにその方法は非推奨とあったので、ビルド時にライブラリをリンクして使用した。単に、libprofiler をリンクすればよく、ソースコードは変更しなくてよい。

実行ファイルを作れたら、

% env CPUPROFILE=XXX ./program

などとして、環境変数CPUPROFILEに結果の出力先ファイル名を指定してプログラムを実行する。上の例だとファイルXXXが作られ、そこにプロファイリング結果が書き込まれる。結果ファイルが得られたら、それを

% pprof --text ./program XXX

のようにpprofコマンドへ渡すと、プロファイリング結果が表示される。結果の見方はドキュメントの「Analyzing Text Output」節に説明がある。

pprofコマンドにも色々機能があるがここでは割愛。図を作成できたりして楽しい。

64ビット環境での問題

google-perftoolsのREADMEには64ビット環境ではクラッシュすることがあると書かれている。また、INSTALL(バージョン1.5のアーカイブ内のものではなく、trunkにあるもの)には、64ビット環境ではlibunwindを使うよう書かれているように読めた。

はじめはlibunwindを使うようにgoogle-perftoolsをビルドしていたんだけど、そうすると長時間プロファイリングするとクラッシュしてしまい使えなかった。google-perftoolsのIssue 66に64ビットでの問題が報告されていて、これを読むと、google-perftoolsを --enable-frame-pointers でビルドすると一応動く感じ。試すとクラッシュしなくなったので、これでいいことにする。

具体的には、

  • google-perftoolsをビルドする際、configureに --enable-frame-pointers を指定する。
  • プロファイリング対象のプログラムをビルドする際、コンパイラに -fno-omit-frame-pointer オプションを指定する。

とする。使用するライブラリも -fno-omit-frame-pointer オプション付きでコンパイルすべきなようだけど、そうしなくてもコールグラフを構築できているみたい。あるいは、おれの使用している環境のライブラリが -fno-omit-frame-pointer オプション付きでコンパイルされていたものなのかもしれない。

マルチスレッドでの問題

CPUプロファイラのドキュメントには

In Linux 2.6 and above, profiling works correctly with threads, automatically profiling all threads.

と書かれているが、Linuxのカーネル2.6.9上でマルチスレッドアプリケーションのプロファイリングを行うと、メインスレッド以外をプロファイリングしてくれなかった。2.6.31上で試すと大丈夫っぽい。

プロファイル測定というページに、カーネル2.6.11以前ではgprofがマルチスレッドプログラムを正しくプロファイリングできないと書かれていたので、これと同じ原因なのかなと考えている。

それで、google-perftoolsのソースを読んでみたら、google/profiler.h で宣言されている ProfilerRegisterThread() を、スレッドが生成されたときに呼び出せば古いカーネル上でもうまく動きそうに思えた。試してみると想定通りに動いた。

試した結果をメモ。最後に示すプログラムをプロファイリングした結果である。

  • カーネル2.6.9、ProfilerRegisterThread()なし(ソースの(A)の行をコメントアウト)。
Total: 192 samples
     192 100.0% 100.0%      192 100.0% run
  • カーネル2.6.9、ProfilerRegisterThread()あり。
Total: 581 samples
     195  33.6%  33.6%      195  33.6% loop0
     194  33.4%  67.0%      194  33.4% loop1
     192  33.0% 100.0%      192  33.0% run
       0   0.0% 100.0%      389  67.0% Worker::operator
       0   0.0% 100.0%      192  33.0% __libc_start_main
       0   0.0% 100.0%      389  67.0% boost::detail::thread_data::run
       0   0.0% 100.0%      192  33.0% main
       0   0.0% 100.0%      389  67.0% thread_proxy
  • 使用したプログラム。
#include <iostream>
#include <boost/lexical_cast.hpp>
#include <boost/thread.hpp>
#include "google/profiler.h"

using namespace std;
using namespace boost;

double loop0(long count) {
    double result = 0;
    for (long i = 0; i < count; ++i) result += i + 0.1;
    return result;
}

double loop1(long count) {
    double result = 0;
    for (long i = 0; i < count; ++i) result += i + 0.1;
    return result;
}

class Worker {
    typedef double (*Function)(long);
    long count;
    Function function;
public:
    Worker(long c, Function f) : count(c), function(f) {}

    void operator()() {
        ProfilerRegisterThread(); // ---- (A)
        cout << function(count) << endl;
    }
};

double run(long count) {
    Worker w0(count, loop0);
    Worker w1(count, loop1);
    thread t0(w0);
    thread t1(w1);
    double result = 0;
    for (long i = 0; i < count; ++i) result += i + 0.1;
    t0.join();
    t1.join();
    return result;
}

int main(int argc, const char* argv[]) {
    if (argc != 2) {
        cerr << "error" << endl;
        return 1;
    }
    run(lexical_cast<long>(argv[1]));
}

[dev][link] プロファイラの仕組み

プロファイラの仕組みについて、次のページが参考になるのでメモ。

やること

  • 請求書
  • 見積書
  • クリーニング