HSPで実験用プログラムを作るときのあれこれ

HSP3 検証中!

HSPで分解能1ms以下の時間計測ができるか

結論を先に述べると,できる。 かなり昔から私ができる,できると謳い回っているのに,どうして信じないのか。できるんです! これについては2003年からはプログラミング講習で毎年しゃべっているので, これまではわざわざ記事を作ろうとは思わなかったのだが, より広い範囲向けとして書いてみよう。

もちろん,私が本格的に HSP を使い始めたころ(v2.4xからv2.5になったころ)は, 配布されている HSP パッケージに付属のものだけではできなかったので自作のプラグインを使っていたが,不可能ではなかった。 loadlib が標準添付されるようになってからは付属のものだけでできるし,HSP3 になってからは外部プラグインすら何も要らない。 本体の機能だけで1ms測れます! よって,できないなんて悪いウワサ (たとえばこのようなサイト(2006-06-12確認)とか) はデタラメです。いいですか,HSP だけでできます!

このようなウワサが流れるのは,単に市販コンピュータの動作法や HSP のことを知らないことが原因だと思われる。 むしろそのような人でもふつうに扱えてしまうのが HSP のすばらしいところだと考えるべきかな。 HSP の標準命令には確かに時刻を得たり,待機したり,時間に関係するものが含まれているので, それでなんとかできるんじゃないかと試してみて,失敗して,「できない」と言う。 (まあ,失敗を認識しているだけマシなんですが。) そのようなユーザと,Win32API 直書きしてプログラミングしているような人との間には大きな知識のギャップがあるわけで, これは仕方ない。知れば良いだけである。

時間を計測するためのプログラミング方法はそれこそ数え切れないほどあるが,Windows 上でふつうに走らせるアプリケーションで, 特殊なデバイスを使うことなく時間経過量を得ようと思ったら,取れる手段 (Win32API) はおよそ以下の4つのどれか(あるいはその組み合わせ)だろう。 他にも時間に関係するものはあるのだが(タイマーメッセージとか),実験などでは実用に耐えない。

実際に使うとすれば, timeGetTime か QueryPerformanceCounter であろう。 Sleep を待機でなく計測に使うのは無茶なので。また, timeGetTime があれば GetTickCount はお払い箱なので使わない。 ちなみに,VC++の clock 関数は内部で GetSystemTimeAsFileTime を使って実装されているので,GetTickCountより劣る。 心理実験で使うくらいならms単位で64bitsは要らないし。

timeGetTime であれば(そして最近のPCであれば) 分解能1msで測定できる。 (カウンタなのでこの最後の1msは実際よりわずかに異なると見るべき。) QueryPerformanceCounter を使えば,もっと細かな精度で測定可能である。どれくらいまでいけるかはマシンの性能に依存する。 時間経過量の計測というのは,簡単に言えば,これらをコールしてその瞬間のカウンタ値を得て, 複数の時点間での差分を計算すればよいだけである。

実際にどれくらいの呼び出し負荷があるのか

timeGetTime や QueryPerformanceCounter を呼び出せばよいだけ,とは言っても,コンピュータをわかっている人なら, その呼び出し自体にいくらか時間がかかるのではないか,と疑問を挟むだろう。もしそうなら厳密な測定に影響が出る,と。 その疑問はもっともである。

確かに呼び出し自体に物理的時間をいくらか要する。これは当然である。 しかし,そのことがすなわち障害に直結するわけではない。 問題は,その要する時間がどの程度か,である。

ということで,疑り深い方々のために実際に測ってみた。また,上記のAPIを利用するにも, HSP3 になってからは手段が一気に増えたので,それらの違いもついでに比べてみる。テストコード。

まずは timeGetTime 。 比較される方法は下の表にあるとおり。 プラグインはv2型,v3型ともに C++ (Visual Studio 2003) で実装し,内部で timeGetTime API をコールしている。 各方法の詳細は HSP スクリプトと C++ ソースを見ていただけばすぐわかるだろう。 表中のデータは,各方法で1,000,000回反復してカウンタ値を取得し,それにかかった時間を計測し, これを1試行として16試行での平均値である。テストは P3M 800M, ACPI, WinXP SP2 のラップトップで行った。

平均所要時間(ms)SD
HSP v2.x プラグイン
v2互換の#func指定 タイプ1904.80.95
v3での引数型指定(だがv2と同じく引数4つ)903.91.41
v3での引数型指定(引数1つだけ)593.10.78
HSP v3.0 プラグイン
命令397.30.77
関数637.12.42
システム変数588.51.37
プラグインからエクスポートされた関数592.70.58
直のAPIコール
#deffunc によるユーザー定義命令1941.11.39
#define によるマクロ814.60.70

このデータの比較から以下のことが言えるだろう。

  1. v3 の新しい方法の中では,命令として呼び出すのが一番速く,次にシステム変数,関数の順である。

    これを見てしまうと,新たに導入し旧来の命令を置き換えた「関数」のどこがよいのかわからなくなってくる。 組み込みのものは違うのだろうか。

  2. #func で指定する引数の数が影響する。

    DLL側での関数定義の引数には関係なく(これは当然か), HSP 側の (実際に命令にいくつパラメタを書くかではなく) #func で指定した引数型にあわせてスタックに引数を積みコールされているからだろう。

  3. API (winmm.dll の) を HSP から直に呼び出すのは,内部で API をコールする自前DLLのエクスポート関数を呼ぶより遅い。

    これはおそらく,HSP から winmm.dll の timeGetTime を呼ぶと戻り値をシステム変数 stat に入れるからだと思われる。 このstatへの代入とそこから任意の変数への代入が二度手間で,引数(変数へのポインタ)を通じて返すよりコストがかかるのだろう。

  4. #deffunc は他と比べて倍以上に時間がかかる。

    とはいえ,マクロと比べても単純計算で 1回の呼び出しにつき1μs程度多くかかるだけであるから, #module + #deffunc の有用性を考えれば,#deffunc はすぐに捨て去るべき選択肢ではない。

結果としては,HSP3 で新しく追加された機能である, プラグインで新たな「命令」を実装する方法が一番速く,これを使うのがベストである。 ただし,これらの方法間の差は大きくても1回の呼び出しにつき 1~1.5μs 程度であり, 1msの分解能での測定を試みる timeGetTime の利用上では,まったく問題にならない差である。 実質的にはどの方法を使っても構わない。表中の最後の2つの方法は特別なプラグインを必要としないから, 確かに,プラグインなしでも分解能1msの測定はできるのである!

次に QueryPerformanceCounter。 今度はv2プラグイン関係は削って,これまた新しく導入された #cfunc を試してみた。

平均所要時間(ms)SD
HSP v3.0 プラグイン
命令1737.61.54
関数1957.42.16
システム変数1889.01.61
プラグインからエクスポートされた関数
#func1831.82.28
#cfunc2018.71.30
直のAPIコール
#deffunc によるユーザー定義命令3060.81.73
#define によるマクロ1783.61.98

全体的に timeGetTime よりも時間がかかっている。 おそらくこれには,カウンタの読み出し速度だけでなく, カウンタ値が64bitsであるのと,そのためのプラグインの内部処理に起因する分も含まれているはずだ。

timeGetTime の場合と同様,どれでも大差はないという結果になった。 ただ今度は,直にAPIコールしたときがv3命令なみに速い。これは,timeGetTime の場合とは違って, QueryPerformanceCounter が引数にポインタを取るタイプのAPIだからだろう。戻り値でカウンタ値を得るのではないので, timeGetTime のような回りくどい手順は要らないのだ。

まあとにかく,こちらでも1回の呼び出しにつき 2μs 弱のコストなので, μs レベルの分解能を期待しないならばプラグインなしで十分であるし,たとえ期待する場合でもプラグインでは解決できない。 そのような場合は何か他の手段が必要である。

従って,一つの明確な結論として,通常の HSP での経過時間の計測にはプラグインは要らないと言える。 あ,もちろん,時間計測に当たっては,その他のいろいろな条件を満たしているか (例えば,強烈に CPU を食うプロセスを同時に走らせていないか,とか,上述のテストマシン程度の性能の PC を使っているか,とか) 注意しておく必要はあるが,それは PC で心理学実験をするとき一般に言える話なので,ここで格別に強調するまでもない。

  1. first written on 2005-08-10
  2. improved the test codes so that values in tables slightly changed (but those have no impact on the conclusion) on 2007-06-01

Mersenne Twister for HSP3

高品質で有名な(擬似)乱数生成法である, 松本眞さんと西村拓士さんのメルセンヌ・ツイスタ ( Mersenne Twister with improved initialization (2002) : mt19937ar.c ) を, HSP3 向けに移植しました。

メルセンヌ・ツイスタによる乱数生成は,HSP3 からは添付の拡張プラグイン HSPDA.DLL で提供されていますが, 乱数生成だけのためにプログラムにプラグインを1つ添付しなければならないのが嫌な人もいると思われるので, プラグインを使わず HSP スクリプトだけで実装するという企図で作成しました。

mt19937ar.as

このスクリプトはオリジナルのC言語ソースをそのまま移植しているので, 初期化や乱数の取得において幅広い応用ができます。現在 (HSP3.0) の HSPDA.DLL では, 現在時刻による初期化と,半開区間を指定した実数もしくは整数の乱数の取得しかできません。 HSPDA.DLL の rndf_* 命令に対してこのスクリプトが優れている点は,例えば,

ここに書いたことはすべてオリジナル の受け売りです。移植しただけなので。

このスクリプトはもっと高速化ができるかもしれません。 利用者がスクリプトを修正した場合のために,オリジナルと出力を比較するテストコードもつけてあります。 バグや高速化法を見つけた方はご連絡いただければありがたいです。

スクリプトを修正するにあたって注意すべき点は,

  1. first written on 2006-08-24

バージョン 2.x のときのノート