オプション沼(4) gccに-pgオプションつけて、アッカーマン関数の再帰の回数を数えてみる

Joseph Halfmoon

今回は使う人もいれば使わない人もいる? -pg オプションを gcc にお願いしてみます。プロファイラ gprof を使うときに必要なオプションです。プロファイリングそのものはコンパイラが「仕掛けをし」オブジェクトが実行時に自前でデータ収集、gprofはそれをまとめてレポートしてくれるという感じでありますが。

※動作確認に Windows11 WSL2上のUbuntu 20.04 LTS を使用しています。

プロファイリング

ソフトウエアの「どの辺」が時間がかかっているのだか、呼び出し回数が多いんだか、調べるのに使うプロファイラには大きく2つの方式があると思います。

一つは今回使用してみた gprof のように、コンパイル時に「仕込み」をいれておき、実行時に「自前」で各部実行時間とか実行回数を計測するもの。回数は正確でしょうが、計測自体にかかるオーバヘッドが含まれるので計測しないときと真の実行時間というか、動作タイミングというかは異なってしまいます。ただね、お手軽。ぶっちゃけ今回みたいにオプション付きにコンパイルすればOKっと。

もう一つは、割り込みあるいはCPU内蔵のパフォーマンスカウンタなどプログラムの外側の機能を使って計測を行うもの。対象ソフトウエアそのものは普段の実行時とかわらず「自然な」状態での計測が可能です。割り込みの場合は多少オーバヘッドが気になりますが、CPUのパフォーマンスカウンタ利用の場合は、CPUがソフトウエアのオーバヘッド極小で、普段目にすることのない数値までアカラサマにしてくれるのでうれしいです。ただ、測定前にプロファイラ様にいろいろ設定をしてから、いざ計測という感じです。メンドイ?

本サイトで gprof つかっている記事を調べたら何件かヒットしました。一番最近では以下でした。gprofの使い方を思い出している一件。

ソフトな忘却力(24) 久しぶりの gprof、簡単なところから思い出す

また、Gnu gprof のマニュアル(多分古い)は以下に。

GNU gprof

Ackermann関数

呼び出し回数が多くて、実行時間がそこそこかかるもの、という観点で今回使用してみたのが、皆大好き Ackermann関数です。再帰的(頭山的)に呼び出すコードは極めて単純明快ですが、ちょっと気を許すと「帰ってこなく」なる気がするアレです。

#include <stdio.h>
#include <stdlib.h>

int ackermann(int m, int n) {
    if (m == 0) {
        return n + 1;
    } else if (n == 0) {
        return ackermann(m-1, 1);
    } else {
        return ackermann(m-1, ackermann(m, n-1));
    }
}

int main(int argc, char const *argv[]) {
    int m,n;
    if (argc != 3) {
        fprintf(stderr, "Ackermann function: A(m, n)\n");
        exit(EXIT_FAILURE);
    }
    m = strtol(argv[1], NULL, 10);
    n = strtol(argv[2], NULL, 10);
    printf("A(%d, %d)=%d\n", m,n,ackermann(m,n));
    exit(EXIT_SUCCESS);
}

希望としては「数秒」くらいで結果が出て完了してほしいです。すると以下のA(3,13)とか、A(4,1)くらいがいいところじゃないかと思います。それ以上を求めると何時になったら帰ってくるのかわかりませぬ。3の13がこちら。

ackermann_3_13

4の1がこちら。

恐れを知らぬよゐこはもっと大きな数で挑戦してね。知らんけど。

-pg オプション

gcc に gprof 用にデータ収集の「仕込み」を入れてねとお願いするのが -pg オプションであります。とりあえず「最適化なし」の-O0オプションと併用のコマンドラインが以下に。

$ gcc -pg -O0 -o ackermann ackermann.c
$ ./ackermann 3 13

上記の実行後のディレクトリをみると、何やら gmon.out なるファイルができております。これぞ -pg オプションの結果である実行時計測のデータファイルみたいです。

gmonOut

gprof は、オブジェクトファイルと同時にこのgmon.out ファイルを指定して実行することで、有用な情報を提示してくれます。まず、gprof の -p オプションの結果の先頭部分が以下に。

gmonP

ああ、gprof -p は、アッカーマン関数には適しませんな。なにせackermann()は、一度呼び出されると、あとはひたすら自分自身を呼び出しつづけるので、-pではただ1回の呼び出しで全体時間の82.72%を使っていることが分かるのみ。多数の関数が関与していて、それら関数の中で「重い」奴を見つけ出すときには有効な -p ですが、アッカーマンは相手が悪い?

それに frame_dummy って何。そんな関数書いてないぞ。これはツールチェーンが密に挿入してくれている関数らしいっす。その中身はまたそのうち。今回はパス。

今度は同じ gmon.out でも gprof -q でお願いしてみます。「コール・グラフ」を表示してくれるオプションっす。結果の先頭部分はこんな感じ。

gmonQ

おお、ackermannの3の13だと約28億6300万回も実行されているのね。ackermann関数をビーグルにgprofしてみたかいがあるというものです。

ついでなので深入りは避けますが、最適化オプションを -O3 に変えてみたものが以下に。コンパイルと実行のコマンドラインが以下に。

$ gcc -pg -O3 -o ackermann3 ackermann.c
$ ./ackermann3 3 13

gmon -q の結果(の先頭部分)が以下に。gmonQ_O3

 

オプティマイズの効果わかりやすいです。14億3146万回と関数の呼び出し回数が半減しました。どうやってそうしているのかはまた今度だな。

オプション沼(3) 何は無くても pkg-configがなんとかしてくれる?かも? へ戻る

オプション沼(5) gccに-Sオプションつけて、アセンブラ出力を愛でてみる へ進む