番外企画・速度にまつわる小ネタ達の巻
どんな言語でも、プログラミングをする際に要求されることの一つに「いかに高速なプログラムを組めるか」があります。今回はその当たりに突っ込んでいきたいと思います。でもネタがなくてベンチマーク大会に走ったわけではないぞなもし(^^;。
4/26補足:トップメニューの変更に伴って「番外企画」となりました。でも別に何も変わってないんですけど。
●概要
一般に「こうすると速くなる」といわれている高速化テクニックについて以下のスクリプトを用いて処理にかかった時間を測定し、その効果を確かめます。間にあいている3行の空行に計測するスクリプトを挿入して使います。なお、このスクリプトをご家庭(笑)で使用するには、MIAさん作の「DSOUNDEX.DLL」が必要になります。
#include
"DSOUNDEX.AS" repeat 5 timer START=stat timer FINISH=stat TOTAL+=FINISH-START loop SYOSU=(TOTAL\5)*2 TOTAL=TOTAL/5 mes "time :"+TOTAL+"."+SYOSU stop |
今回実験に使用したマシン環境は、以下のようなものとなっています。
機種:PC-9821 Xa13
CPU:Pentium 133MHz、K6-2 333MHz
メモリ:80M、128M
OS:Windows98
画面:1024*768 256色
HSP:Ver 2.4g、Ver 2.4h2、Ver 2.5
いうまでもないですが、計測中はマウスやキーボードには一切手を触れていません。また、計測時はHSPスクリプトエディタ以外のアプリケーションを最小化した状態で計測しています。
●実験開始
実験1.「a=a+1」より、「a++」「a+=1」のほうが速い
ってプログラミングリファレンスなどに書かれているので、間違いないと思いますが実験。それぞれの計算を100万回行うのにかかった時間を比較します。
a=a+1 | a++ | a+=1 |
4439.8ms | 2059.8ms | 3030.6ms |
なんとa++だと普通の倍の速さで計算できるではありませんか!! またa+=1でも約25%の高速化。
念のため、引き算でもやってみました。
a=a-1 | a-- | a-=1 |
4447.6ms | 2068.6ms | 3027.0ms |
ほぼ同じ結果となりました。
結論:a++、a+=1の方が高速に計算できる。
実験2.割り算をビットシフトで代用すると速くなる
C言語では2のn乗で割るのならビットシフトを使った方が高速に処理ができるのですが、HSPではどうか試してみようということで実験。1同様計算を100万回行うのにかかった時間を比較します。
a=a/2 | a=a>>1 |
4810.0ms | 4542.0ms |
あらら、あまり変わらないみたい。では掛け算で比較。
a=a*2 | a=a<<1 |
4537.8ms | 4542.4ms |
アラららららららら、逆に遅くなったぞ。ビットシフトって役に立たないってこと? ガーン(注:使い方によっては十分役に立ちます)。
結論:ビットシフトは高速化としては微妙な効果しかなく、しかも掛け算だと逆に遅くなる(ただし掛け算はオーバーフローするので使い分けが必要)。
実験3.clsで消すよりboxfで消した方が速い
画面消去をboxfで行うのとclsで行うのとどっちが速いでしょうか。ここではそれぞれ500回画面を消去して比較してみます。ウィンドウサイズは640*480で、redrawは使っていません。
cls | boxf 0,0,640,480 |
52334.0ms | 12194.6ms |
うわ〜全然速度違うじゃないか。たぶんこれはclsがパレットやオブジェクトを初期状態に戻していることが原因だと思いますが。
結論:boxfの方が断然速いが、オブジェクトを使っている場合は初期化しないと残っているので注意。
実験4.redrawはどれくらいで遅くなるか?
redrawは、1フレーム当たりの描画量が少ないときは使わない方が高速に動作するのですが、ではどれくらいまでならredrawを使った方が速いのか実験してみました。ドットを1フレームに1個〜100個描画、これを100フレーム行ってかかった時間を大比較!! なお、ゲームプログラミングを想定しているため画面はboxfで消去しています。
1個 | 2個 | 5個 | 10個 | 25個 | 50個 | 75個 | 100個 | |
redrawなし | 2502.8ms | 2565.8ms | 2602.4ms | 2838.8ms | 3130.0ms | 3769.4ms | 4425.2ms | 5073.6ms |
redrawあり | 2480.8ms | 2508.6ms | 2514.6ms | 2504.2ms | 2570.8ms | 2606.2ms | 2708.8ms | 2752.2ms |
・・・って全然遅くないやん(爆)!! っつうか、画面消去して一から書き直すくらいならredraw使うに決まってますから実験失敗。気を取り直して今度は画面消去無しで測定してみます。
1個 | 2個 | 5個 | 10個 | 25個 | 50個 | 75個 | 100個 | |
redrawなし | 36.6ms | 53.0ms | 132.8ms | 257.8ms | 628.8ms | 1255.0ms | 1912.6ms | 2482.0ms |
redrawあり | 1837.4ms | 1801.0ms | 1841.8ms | 1825.0ms | 1916.6ms | 1993.8ms | 2038.4ms | 2083.0ms |
凄い結果が出ちゃいました。画面消去しなかったらredraw使っちゃダメという結論が出ました。ただしこれはドットを打っているだけなので、boxfやgcopyに置き換えてさらに実験を行ってみます。まず、32*32ドットの四角を描いて比較。
1個 | 2個 | 5個 | 10個 | 25個 | 50個 | 75個 | 100個 | |
redrawなし | 44.2ms | 90.6ms | 206.6ms | 420.0ms | 1020.0ms | 2024.0ms | 3063.0ms | 4070.0ms |
redrawあり | 1840.2ms | 1811.4ms | 1878.4ms | 1891.8ms | 2051.8ms | 2247.4ms | 2428.4ms | 2680.8ms |
続いてgcopy。gmodeを2にして、画像を32*32ドットコピーして比較。
1個 | 2個 | 5個 | 10個 | 25個 | 50個 | 75個 | 100個 | |
redrawなし | 45.6ms | 101.2ms | 240.6ms | 475.4ms | 1202.8ms | 2362.8ms | 3567.4ms | 4791.0ms |
redrawあり | 1808.0ms | 1863.6ms | 1868.2ms | 1984.6ms | 2232.8ms | 2745.2ms | 3074.0ms | 3495.6ms |
結論:非リアルタイムゲームなどのようにフレームごとに画面の大部分を書き替えないプログラムであればredrawを使わないほうがよい(ビデオカードの性能や見栄えを考慮した場合などによってはその限りではないが)。
実験5.サブルーチンは遅い!?
MSXを使っていたころに「サブルーチンを多用すると処理速度が低下する」という話を聞いたことがありますが、HSPではどうか大比較。実験1のa++を100万回行うプログラムを、サブルーチンを使った場合とサブルーチンをgoto文で代用した場合とで比較してみます。参考までに、goto文で代用するとどうなるかを下に記しておきます。なお、サブルーチンを使わずに処理した場合の結果は、実験1にありますので割愛させていただきます。
goto *sub *ret *sub |
サブルーチン | goto文で代用 |
4071.2ms | 4331.2ms |
倍近くの時間がかかっています。あと、サブルーチンをgotoで代用するとさらに遅くなっています。いくら何でもループの中でgotoを使うというような行為はエラーの原因にもなりますし、そもそもこういうプログラムを組むと凄いかっこ悪いのでやめましょう。
結論:遅い。goto文で代用するともっと遅い。
実験6.ループはやっぱりrepeatのほうがよいのか
repeat〜loopを変数とif文で代用すると、速度はどう変化するのか確かめてみました。プログラムは例によってa++100万回です。repeat〜loopの結果は実験1にあるので以下略。
変数で代用 |
6819.4ms |
ぐは〜劇的に遅くなりました。repeat〜loopの約3倍半です。repeatがネストし過ぎていない限りやっちゃいけないですな。
結論:むちゃくちゃ遅くなる。
実験7.if文と理論式はどちらが速いのか
理論式というのは「a=a+(x>200)」のような式のことで、この場合「xが200を越えている場合、aに1が足される」という意味になります。BASICではよくどちらが速いだの遅いだのいわれ続けましたが、HSPでは結局のところどうなのか実験。「a>10のときに、bを1足す」というプログラムを100万回行って速度を比較してみました。
if文 | 理論式 |
4274.0ms | 4540.2ms |
というわけでif文に軍配!! だいたいHSPだと理論式を使ってプログラムをスマートにする意味がほとんどないんだよね。
結論:if文を使おう。
実験8.\と&はどちらが早いか(4/7追加)
演算子「a\b」はaをbで割ったあまりを出すというもので、「a&b」はandの論理演算です(主に特定のビットを0にするために使われる)。これらの演算子は読んでの通り使い方が異なるわけですが、ある条件ではどちらを使っても同じ動作をします。であれば速いほうを使うべきだろってことで比較です。
a\b | a&b |
4567.2ms | 4268.6ms |
100万回やって0.3秒程度の差ではありますが、速くなることにかわりはありません。条件を書かずに使えっていうのも変ですけど。
結論:&の方が速くなる。
実験9.ループはやっぱりrepeatのほうがよいか・パート2(4/26追加)
実験6においてループはrepeatにしたほうが3倍半くらい速くなると言う結果がでました。そこで、ゲームなんぞのメインルーチンをrepeatでループさせればどのくらいまで高速化できるのかという、どこまでもセコイ実験です。タイマーのチェックとawait命令だけを延々繰り返し、10秒後の平均フレーム数(fps)を比較します。
goto命令 | repeat命令 |
23737fps | 23482fps |
あれっ、gotoの方が速いぞ。どーゆうこっちゃ。
結論:goto文のほうが速いけど、repeatの方が少し速度が安定している感じだ。
実験10.i=cntは遅い?(2001/01/12追加)
以前ループ中にあった無意味な一時変数へのカウンターの代入を消したところそこそこの高速化が望めたことから変数への代入に結構なオーバーヘッドがあることに気づいたので実験してみることにします。例によって100万回ループの中で「a+=cnt」あるいは「i=cnt:a+=i」を実行してどれくらいの差が出るか、またループの中でカウンターを参照する回数を増やして(「a+=cnt」や「a+=i」の回数を増やして)一時変数の使用は効果があるのかを検証しました。
1回 | 2回 | 3回 | 4回 | 5回 | 6回 | 10回 | |
カウンター直接参照 | 1268.0ms | 2349.6ms | 3295.8ms | 4358.4ms | 5382.8ms | 6287.0ms | 10283.8ms |
一時変数に代入後参照 | 2087.0ms | 2865.6ms | 3675.6ms | 4464.0ms | 5432.0ms | 6156.8ms | 9399.0ms |
速度差 | 819.0ms | 516.0ms | 379.8ms | 105.6ms | 49.2ms | -130.2ms | -884.8ms |
こうなりました。5回までは直接参照の方が有利ですが、回数が増えてくると一時変数に代入した方が高速になるようです。
結論:たくさん使うなら代入しておいた方がよい。
実験11.ネストってどうよ(2001/01/12追加)
例えば64*64の配列を参照するときに普通はループを二重にして処理しますがこれをネストさせないようにするとどのくらい速度は変わるのでしょう。一万*100回のループをネストした場合としなかった場合で比較してみます。一応ネストした場合は一番深いループのカウントを一時変数に代入した場合としなかった場合も調べます。
ネストなし | ネストあり(代入あり) | ネストあり(代入なし) |
5110.0ms | 2728.8ms | 1893.8ms |
除算が遅いせいだと思いますが、全然違いますね。ちなみに一時変数への代入については前の実験を参考にして決定しましょう。
結論:ネストしとけ。
実験12.コメントの通過は時間がかかるか?(2001/01/12追加)
内部の動作を考えればこの比較が無意味なのは明らかですが、一応はっきりさせておきたいので比較します。
コメントなし | コメントあり |
259.0ms | 259.0ms |
あ、全く一緒だ。ちなみにこの後コメントや空行を100行くらい入れましたが誤差の範囲内だったことを付け加えておきます。
結論:なワケねぇだろ。
●実験を終えて(ってなんだか論文かレポートみたいだな。笑)
というわけで今回は処理速度にまつわる実験を行いました。知らなかった人はこれを参考にできる限り高速で動作するプログラムが組めるようになれば幸いだよね。でも無駄のないプログラムを組むことが一番高速化に繋がる気がするけど。今回やったことって基礎知識みたいなところあるし。
3/15補足:MIA's Homepageにて、高速化をテーマにしたページが最近できたようです。ここよりもさらに深くつっこんでいるので、一度目を通しておくとよいでしょう。念のためいっておきますが、やり始めたのはこっちの方が先だから「おまえパクッただろ」とか言わないでね(笑)。
4/26補足:番外企画に移動したこともあって、実験はどんどん増えていきます。ネタがあればね。