自分のスクリプトのスピードが十分に出ないというときには、 性能を改善するポイントとして考えられる点がいくつかあります。
どんなときでも、実行時間を食いつぶしているコード部分を見つけるというの
を先ず最初にやるのがよいでしょう。Gauche にはこの作業を補助する基本的
なツールが2つあります。組込のサンプリングプロファイラ(これについては次
の節で説明します)を使えば各手続きでどれほどの時間がかかり、その手続き
が何回呼ばれたかを表示できます。gauche.time
モジュール
(gauche.time
- 時間の計測) ではコードの中の特定の部分の実行にかかる時間を
測定するためのAPIが提供されています。
最適化というのは特殊化ということでもあります。もっともよく使われる 実行のパターンを探して、そこを効率よく実行する専用のパスを設けることで す。Gauche 自身も例外ではありません。したがって、Gaucheが効率よく実行 できるパターンがいくつかありますし、また一方では効率よく実行できないパ ターンもあります。次の節では、パフォーマンスに関するヒント では Gauche が効 率良く実行できるパターンにコードをあわせるチョットしたコツを教えましょう。
• プロファイラを使う: | ||
• パフォーマンスに関するヒント: |
0.8.4 から Gauche は組込みのプロファイラを備えています。これは 現時点ではまだ実験的なもので、Linux 上でしかテストしていません。すべてのプ ラットフォームで利用できるわけではありませんし、シングルスレッドの アプリケーションでしか動きません。
非対話環境でこのプロファイラを使うには、gosh のコマンドラインオプショ
ンとして -ptime
を指定してください。
% gosh -ptime your-script.scm
your-script.scm の実行完了後、Gauche は各関数についてその呼び出し回数 および消費時間を示した表を印字します。この表は総消費時間の順でソートさ れています。
Profiler statistics (total 1457 samples, 14.57 seconds) num time/ total Name calls call(ms) samples ---------------------------------------------------+------+-------+----------- combinations* 237351 0.0142 337( 23%) (lset-difference #f) 1281837 0.0020 256( 17%) (make-anchor make-anchor) 3950793 0.0005 198( 13%) member 4627246 0.0004 190( 13%) filter 273238 0.0030 81( 5%) every 1315131 0.0004 59( 4%) (lset-difference #f #f) 1281837 0.0004 54( 3%) (make-entry make-entry) 730916 0.0005 40( 2%) (clear? #f) 730884 0.0005 33( 2%) (initialize #f) 599292 0.0005 32( 2%) fold 237307 0.0013 30( 2%) acons 806406 0.0004 29( 1%) clear? 33294 0.0084 28( 1%) (combinations* #f) 805504 0.0002 15( 1%) (make-exit make-exit) 730884 0.0002 15( 1%) lset-difference 237318 0.0006 15( 1%) reverse! 475900 0.0001 6( 0%) (fold <top> <top> <list>) 237323 0.0003 6( 0%) procedure? 238723 0.0002 4( 0%) pair? 237307 0.0001 3( 0%) : :
時間プロファイラは統計的標本化をおこなっていることに注意してください。 プロファイラは10ミリ秒ごとにプロセスに割込んで、その時点で実行されてい る関数を記録します。ナノ秒オーダの関数呼出しごとの個別の実行時間に比べる と、このサンプリングレートはかなり粗いものです。しかしながら、プロ グラムの実行時間が長ければ、各関数ごとの標本分布は関数ごとの消費時間を ほぼ反映しているだろうと期待できます。
数字はあくまで近似にすぎないこと を心にとめておいてください。ひとつの関数あたりの標本数はプログラムが扱 うデータが違えば、すぐに変化してしまうことがあります。 また、今のところGCにかかる時間はGCがトリガされた関数の実行時間に 算入されてしまっていることに注意して下さい。これによって、あまり 重要でない関数がリストの上位に浮かびあがってくることがあります。 一般的なパターンを知るには、プログラムをいろいろなデータで走らせて みると良いでしょう。
一方、関数呼び出し回数のカウントは正確なものです。これは Gauche は 実際の呼出しごとにカウントしているからです。
Schemeでは基本的にすべての関数は無名なので、プロファイル結果の’name’ フィールドはヒントにすぎません。トップレベルで束縛されている関数につい ては通常それが最初に束縛されたグローバル変数名が印字されます。内部関数 については関数の入れ子構造を反映して名前のリストが印字されます。 メソッドは、名前と特定化子のリストとして印字されます。
プロファイラはそれ自身にオーバヘッドがあります。通常は、処理時間が 20-30% 増加します。プロファイラを選択的にオンにしたい場合や、 停止しないサーバプログラムを走らせていて、そのサーバを停止することなく、 統計を取りたいような場合には、プログラムからプロファイラ APIを呼ぶこと ができます。詳細については プロファイラAPI を参照してください。
「論より run 」これがパフォーマンスチューニングの第一法則です。 Scheme のような高級言語では、何がパフォーマンスに強い影響を 与えるかはことのほかその実装に大きく依存し、ある処理系では とても安価な操作が別の処理系ではとても高価になり得ます。 Gauche にもそのようなパフォーマンスに関する実装特有の特徴があり、 それらのうちのいくつかを知っておくことは、ベンチマークの結果の どこに着目すべきかを知るうえで助けになるでしょう。
「ソースコードの2割が実行時間の8割を消費する」というのも古くから 言われています。実際の実行時間に大した影響を及ぼさないところを 下手にいじくってプログラムをわかりづらくすることは避けましょう。 これからいくつかのヒントを述べますが、これらのことを四六時中気にして プログラミングしなければならないということではありません。 むしろ、出来るだけ明瞭でわかりやすいプログラムを心がけ、ループの一番深いところ (もっとも時間を消費するところ)でこれらのトリックを使うのが良いでしょう。
Ports: SRFI-18 (スレッド) の仕様を満たすために
Gauche のすべての入出力基本関数はポートをロックします。
このオーバーヘッドは小単位(例えばバイト毎)の入出力を行なう
アプリケーションでは無視できないでしょう。
入出力基本関数は通常呼びだし毎にポートをロックし、そこからの
下位レベルの入出力はロックのオーバーヘッドの影響を受けずに
行なわれます。ですから read
やread-uvector
などのより大きな単位で入出力を行なう基本関数では問題と
なることが少なくなります。
(注意:これらの基本関数が常にポートをロックしつづけることを
保証するものではないことに注意してください。また、ポートのロックは
競合がほとんど発生しない場合に最適化されています。
ポートへのアクセスが複数のスレッドで競合する可能性がある場合は、
アプリケーション側でmutexを明示的に用いて競合を避けてください。)
ポートロックが実際に問題となった場合、二つばかり対処策が考えられます。
(1) より大きな単位で入出力を行なう。(2) with-port-locking
(ポートとスレッド 参照)を使ってより広範囲でポートをロックする。
文字列: 多バイト文字列の取扱いのため、Gauche では文字列の
変更とインデックスによるアクセスが特に高価な操作となります。
これは意図的な設計です。 Gauche ではこの二つの操作を避けたプログラミングを
推奨しています。 文字列の中の文字を順にアクセスするには
(インデックスを使わずに)文字列ポート(文字列ポート参照)を使うと
より明瞭かつ効率的なプログラムとなり、一方サーチして部分文字列をとり出す
といった操作には、多彩な高レベル関数が用意されています。
(例えば 文字列を扱うその他の手続き、正規表現、
srfi.13
- 文字列ライブラリ 等を参照。) バイト列を表現するのに
文字列を使っていたなら、代わりにユニフォームベクタ(ユニフォームベクタ
参照)を使いましょう。
深い再帰: Gauche の仮想機械(VM)は効率的な ローカルフレーム割り当てのためにスタックを使っています。 再帰が深くなって(プログラムにもよりますが、大体数百回から千回) スタックがオーバーフローするとスタックの内容をヒープに退避するという オーバーヘッドが生じます。 あるデータ量を越えたところでパフォーマンスの 低下が見られたならば、深い再帰がないか調べてみて下さい。
Generic functions: Generic function の持つ動的な性質の ため、これらの呼び出しは通常の手続き呼び出しより遅くなります。 実行時のディスパッチのオーバヘッドだけでなく、 VM コードへのコンパイル時にたいして最適化が行えないためです。 パフォーマンスのために Generic function の利用をどんな場合にも避ける という必要はありませんが、もしある一つの関数が実行時間の大部分を 占めていて、その関数が Generic function を内部で呼び出しているなら それを使わないように変更してみる価値はあるでしょう。
組込み関数の再定義: Gauche のコンパイラはいくつかの組込み関数を(それらが再定義されていなければ) インライン展開します。 基本関数を再定義するのは時には便利ですが、 限られた範囲にとどめておいた方がよいでしょう。 やり方は、再定義をどこか別のモジュールに集めておき、どうしても再定義 バージョンが必要なときに限ってそのモジュール use するというように しておけばよいでしょう。
クロージャの作成: クロージャを作成するとそれが持つ環境がヒープに
コピーされます。 オーバーヘッドは小さいですが、何百万回も呼ばれるような
ループの中で作成されれば無視できなくなるでしょう。そんな疑いがあれば
その関数を逆アセンブルしてみましょう。 Gauche のコンパイラはクロージャの
簡単な解析を行ない生成をなるべく避けるようになっています。そのような場合
局所関数の本体はインライン展開されています。逆アセンブルのコードに
CLOSURE
命令が含まれていれば、残念ながらクロージャが生成されます。
これらのヒント集は完全でないし、Gauche の改良とともに変わっていくでしょう。 ですから、固定された特徴だとは思わないで下さい。 このヒント集は今後、折を見て実装に対応させて更新してゆきます。