PC-98 の DOS 用プロファイラを作った

タグ: #asm  #PC-98  #release 

n2kd 付属の MML コンパイラである n2kc は、大体 C 言語で書かれていて、DOS API の呼び出し部分などに限ってアセンブラを使っている。 コンパイルにかかる時間は、実機だと586機であればほぼ直ぐに完了する程度にはなっている。 しかし、個人的には 486 33MHz くらい、あわよくば 386 20MHz くらいのマシンでもそれなりにサクサク動くようにしたいと思っている。 それは、PC-98 を入手しようとすると386~486機の層がそれなりに厚く「586機以降の性能でないと快適に作曲ができません」というのは結構勿体ない切り捨てだと思われるからなのと、単に自分がよく使っている PC-98 が Ap2 や Xe あたりの機種だからだ。

n2kc の大部分は C で書かれているため、コンパイル時に高頻度に実行されている処理をアセンブラで書き直せば高速化できる可能性がある。 例えば、直感的には、文字列をパースして数値にする関数はアセンブラ化する価値がありそうな気がする。 が、よく言われているようにまず計測ということなので、コンパイラ中で真に多く実行されているパスがどこかを突き止めなくてはならない。 そこで、DOS 用のプロファイラを作ることにした。

実装方針は、定期的に CPU がどのアドレスの命令を実行しているかを観測して集計できればいいので、以下のようなコードを書けばいい筈だ。 本当は最後に、記録結果をリンカ出力のマップファイル等と比較して高頻度に実行されている箇所を見つける作業が必要だが、この機能はプロファイラと別でいいので一旦忘れることにする。

  • システムタイマ割り込みハンドラに、割り込み発生時のスタック上の CS:IP を記録する処理を登録する。
  • 計測対象のプログラムを実行する。
  • 計測対象のプログラムが終了したら CS:IP の記録結果をファイルに保存する。

これを PC/AT 互換機でやろうとすると PIT のチャンネル0のデフォルト割り込み頻度は約 18.2Hz とプロファイラには恐らく足りないので、チャンネル0の割り込み頻度を変更することになるだろう。 割り込みハンドラでは、CS:IP を記録する他に、均して約 18.2Hz になる頻度で定期的に元の割り込みハンドラを呼び出す処理が必要になる。 やたら面倒な処理という程ではないが、書かずに済めば楽なので、とりあえず PIT のチャンネル0がシステムで使われていない PC-98 専用として実装することにした。 また、この方法故、PIT のチャンネル0を何かの用途で使っているプログラムのプロファイルは取れない。

CPU がどの命令を実行している際にタイマの割り込みが発生するかはわからない。 割り込み発生時に計測対象でない、例えば、DOS カーネル中のコードを実行している可能性もあったりするが、計測対象のプログラムを実行中の CS:IP の集計が得られればプロファイラとしての用は成すので、他の部分を実行している場合の CS:IP は全部捨てることにした。 つまり、事前に計測対象のプログラムのコードセグメントアドレスを調べておき、割り込みハンドラでは、割り込み時点でスタックに積まれている CS と事前に調べたアドレスを比べて、同じだった時のみその CS:IP を記録対象とする。 これを行うためには、メモリ上にロードされたプログラムが実行される前にそのコードセグメントのアドレスを獲得しなくてはならないが、以前思い付きで DOS 用デバッガを作ろうとした(当然?完成していない)ことがあり、int 21h (ah=4bh, al=01h) でロードだけができることを調べていたのでこれを使った。

とりあえず動くのを優先ということで、以下のような制約があるが、プロファイラを書いた。 n2kc は複数のコードセグメントを持たないのと、コンベンショナルメモリを 500KiB くらい空けておけばプロファイラと n2kc を動かすのにメモリは十分足りた。

  • プログラム最初の命令の CS の指すセグメント中の命令を実行している時のみ記録対象となる。
    • 複数のコードセグメントを持つプログラムでは、最初の命令があるコードセグメント以外のセグメントの実行の様子は記録できない。
  • CS:IP の記録のために 128KiB のメモリが必要。
    • C 言語風に書くと uint16_t log[65536] をコンベンショナルメモリに用意して、割り込みハンドラで IP に応じて log[IP] += 1 をしているため。
    • EMS や XMS のメモリに記録した方がいいんだろうなとは思いつつ……。

早速 n2kc のプロファイルを取ってみた。 前述の、雑な直観で結構呼ばれていそうだと思っていた文字列の数値化関数は、実際には上から3番目に多い出現回数だった。 そして、それよりも出現回数が多かったのは、1番目が次の行へ進む関数で、2番目が連続した指定の種類の文字を飛ばす関数だった。 n2kc は、コンパイル対象(音色や各パートに対する演奏命令等)を切り替えながら入力された MML を何度も先頭から舐めるように動作するので、現在のコンパイル対象でない部分は全部次の行へ進む関数で飛ばされることになる。 また、空白を記述可能な場所等で特定種類の文字を飛ばす処理を実行している。 という訳で、これら2つの関数の出現回数が多くなるのは考えてみれば当然なのだが、当然の如く見落としていた。 現状プロファイルを取っただけで n2kc の高速化には着手していないが、ちゃんと最初に計測をしてよかった。

実装したプロファイラは Petit Profiler のページ からダウンロードできます。


投稿一覧に戻るトップに戻る