プロファイラを実装する方法 - Mosh 開発

そろそろ簡単なプロファイラが欲しくなったので実装します。
作るプロファイラは gprof などのプロファイラでは知ることのできない Scheme の世界のこと、例えば「どのScheme 手続きの実行に時間がかかっているか?」などを知るためのものです。
作ってみたら意外と簡単だったので情報をまとめておきます。

仕組み

世間で使われているプロファイラがどういう仕組みで動いていて、どのような限界があるか?については、omoさんのプロファイラのしくみを読むと良いでしょう。

必要な機能

gprof などのプロファイラを使っていると以下の2つの機能が必要そうだということが分かります。

  1. 手続きの呼ばれた回数とその名前の表示
  2. 手続きが費やした CPU 時間とその割合の表示

手続きの呼ばれた回数とその名前の表示

厳密なデータが欲しいので単純に VM の CALL 命令時に呼ばれた手続きを記録をします。
記録作業自体が大きな負荷になっては意味がないので、極力単純な仕組みにします。


以下擬似コードですが、こんな感じ。case 文は VM ループの中にあると思ってください。

case(CALL)
{
    static int count = 0;
    samples[count++] = proc; // 今から call しようとしている手続きを配列に格納

samples はあらかじめ確保しておいた、記録用の配列です。


samples が溢れそうになったらどうしたらよいでしょうか。
結局記録したいのは (手続き 呼ばれた回数) のペアなので、その時点の samples を走査して結果を HashTable に記録し、samples を空にしてあげれば良いです。
一つ注意しなければいけないのは、この走査の作業中は後述のタイマーを一時的に OFF にすることです。そうでないとのの作業自体もプロファイリングの対象となってしまいます。

手続きが費やした CPU 時間とその割合の表示

この情報をとるには、定期的なサンプリングが有効そうです。
つまり一定間隔のタイマーで割り込んで、そのとき VM が何をしていたか?を調べるわけです。


より具体的にはタイマーで割り込んで、そのときの VMレジスタやその他の情報から実行中の手続き情報を得ることになります。
ただし単純なタイマー割り込みを使うと、自分以外のプロセスがたくさん存在する場合に CPU 時間をどれほど費やしているかを正しく知ることはできません。
そのため SIGPROF というシグナルを利用します。


以下のような骨組みを持ったコードで情報を記録します。
記録場所や方法は、前述の samples 配列と同じような形になると思います。

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/time.h>

void signal_handler(int signo)
{
    printf("%s\n", __func__);
    // ここで VM の各種レジスタ情報により実行中のクロージャなどを得て記録
}

int main()
{
    struct sigaction act;
    act.sa_handler = &signal_handler; // set signal_handler
    act.sa_flags = SA_RESTART;        // restart system call after signal handler

    if (sigaction(SIGPROF, &act, NULL) != 0) {
        fprintf(stderr, "sigaction failed\n");
        exit(1);
    }

    const int INTERVAL_USEC = 10 * 1000;
    struct itimerval tval, oval;
    tval.it_interval.tv_sec = 0;
    tval.it_interval.tv_usec = INTERVAL_USEC;
    tval.it_value.tv_sec = 0;
    tval.it_value.tv_usec = INTERVAL_USEC;
    setitimer(ITIMER_PROF, &tval, &oval);
    while (1) {
        // VM Loop
    }

    // 後始末
    return 0;
}

出力例

というわけで出来ました。

dekisugi% ./mosh -p match.scm
time%        msec      calls   name
   8           50     906645   generic-assoc                     
   3           20       9022   set-intersect                     
   1           10        269   parse-lambda-vars                 
   1           10       8113   pass1/sexp->iform                 
   0            0     896744   eq?                           
   0            0     108543   $map1                         
   0            0      91196   foldr1                        
   0            0      57297   memq                          
   0            0      55949   append2                       
   0            0      38445   apply                         
   0            0      35247   append                        
   0            0      32543   match:vector-structures       
   0            0      22018   set-union                     
   0            0      21012   set-cons                      
   0            0      10595   list                          
   0            0       8959   pass1/expand                  
   0            0       8869   equal?                        
   0            0       8739   pass3                         
   0            0       8604   find10                        
   0            0       6832   caddr                         
   0            0       6048   length                        
   0            0       5396   map1                          
   0            0       4496   pass3/symbol-lookup           
   0            0       4487   pass3/compile-refer           
   0            0       4457   cdddr                         
   0            0       3636   pass2/optimize                
   0            0       3008   pass1/refer->iform            
   0            0       2824   pass3/compile-arg             
   0            0       2561   $label                        
   0            0       2425   $local-ref                    
   0            0       2327   car                           
   0            0       2324   assq                          
   0            0       2296   pass3/$local-ref              
   0            0       2284   acons                         
  **          580          **   total

課題とか

C++ で書かれた Scheme 手続きを実行中に、タイマー割り込みが起こった場合にうまくハンドリングできない。
専用のレジスタを作ればできなくはないがどうだろう。