番外企画・速度にまつわる小ネタ達の巻


 どんな言語でも、プログラミングをする際に要求されることの一つに「いかに高速なプログラムを組めるか」があります。今回はその当たりに突っ込んでいきたいと思います。でもネタがなくてベンチマーク大会に走ったわけではないぞなもし(^^;。
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 *ret

 

サブルーチン 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補足:番外企画に移動したこともあって、実験はどんどん増えていきます。ネタがあればね。


戻る