パソコン活用研究5番街(Visual Basic、Excel(VBA)、BASIC プログラミング研究)

実行スピードの向上


今のPENTIUM搭載機でQBASICを使うと、これは結構速くて、いやびっくりするほど速くて
BASICで高速アプリなんて・・・、とBASICにケチをつけていた諸兄に、猛省を促したい
ほどの速さですが、確かに一昔前なら、「山」と言えば「川」、「BASIC」と言えば、「インタープリタ
だから遅い」でした。
8ビットCPUの頃はBASICでゲームを作ると、本当にのろくてのろくて、マシン語を使わねばどーしょうも
ないという状況でした。(マシン語を使っても、極限までスピードアップのハイテク技を駆使しないと、
使えるゲームはなかなかできなかった。)

というわけで、当時のパソコン雑誌の2大花形の話題は、「マシン語入門」と「究極のBASICハイテク技」
だったわけです。BASICはBASICでいかに実行スピードの速いコードを書くか、というのがひとつの大テーマ
だったわけです。
今となっては、先述の通りさほど重要なテーマではなくなっておりますが、このテーマを追求すると、BASIC
の向こうにコンピュータの仕組みが透けてみえてくるので、コンピュータをもっと深く知るにはいい教材だと
思います。また、「N88互換BASIC for WIN」はスピードの点ではやや遅いものがありますので、これ向け
には、面白い実践的テーマだと思います。

ただし、この高速化はインタプリタ方式のBASICの場合です。ネイティブコードにおとすBASICコンパイラ
には当然のことながら当てはまりません。

1 FOR ・・・・・NEXT の高速化
FOR・・・・・NEXT は何回も繰り返しをさせる命令ですから、まずはこのFOR・・・・NEXT ループをいかに
高速化するかというのが、大テーマとなります。1000回も10000回もループするプログラムなら、ここの
スピードがおそくちゃ話にならない。
というわけで、FOR・・・・・NEXT ループ高速化のためのチャレンジをいろいろしていきたいと思います。

(1) N88互換BASIC for WIN で実験
まずはN88互換BASIC for WIN用に実験。
できるだけ単純なプログラムということで、変数XとYに1づつ加算するプログラムで実験します。2000回
ループしてその実行時間を3回計測し、平均値をだしました。

[基本型]
まずは、基本形のADD0.BASです。ポイントは変数を
数型整(X%、Y%)にしている点です。
この点はよく覚えていて下さい。(かつては一般に、整数型の演算は早く、実数型の演算は遅いと言われて
きました。<−−過去の常識)
結果は24.6秒です。

<ADD0.BAS>
10 width 80,20
20 cls
25 x%=0:y%=0
30 t=val(right$(time$,2))
40 for i=0 to 2000
50 x%=x%+1:y%=y%+1
60 next i
100 t1=val(right$(time$,2))
110 if t1<t then t1=t1+60
120 print t1-t
130 ' 24.6


[NEXTの後の制御変数名は省略した方が速い]
60行目を next i −−> next に変えて見ました。
23.6秒とわずかですがスピードアップしました。これは昔からBASICで使われる高速化ハイテクです。
他のBASIC(インタプリタ)でもほぼ共通して使える技だと思います

<ADD1.BAS>
10 width 80,20
20 cls
25 x%=0:y%=0
30 t=val(right$(time$,2))
40 for i=0 to 2000
50 x%=x%+1:y%=y%+1
60 next 
100 t1=val(right$(time$,2))
110 if t1<t then t1=t1+60
120 print t1-t
130 '23.6

[ループ内でよく使う変数はプログラムのはじめに定義すべし]
下のADD1B と ADD3を比べてみて下さい。
ADD1Bでは変数X%,Y%をはじめに定義していますが、ADD3 では他の変数の後で最後に定義しています。
ADD1Bでは基本形のADD0とほぼ同じ25.0秒でしたが、ADD3では29.3秒とかなり遅くなっています。
これはかなりのスピードダウンです。変数の定義の順番でこんなにスピードに差がでるとは、少しびっくり
ではありませんか。なぜ、こんなに差がでたのでしょうか。

一般のBASICインタプリタでは変数は定義された順にメモリの変数領域に格納されます。そして、プログラムで
再び変数が登場すると、変数領域のはじめから目的の変数をさがしていきます。従ってFOR・・・・NEXTループ
でよく使う変数は、できるだけ早めに定義しておいた方が、BASICインタプリタの探す時間が省けて、スピードア
ップにつながるのです。
N88互換BASIC for WINの内部仕様がわからないので、これは推測ですが、結果からみて、N88互換
BASIC for WIN でも一般のBASICと同じ仕組みで、変数の格納、サーチをしていると思われます。

<ADD1B.BAS>
10 width 80,20
20 cls
21 x%=0:y%=0
22 a=0:b=0:c=0:d=0:e=0:f=0:g=0:h=0:j=0:k=0:l=0:m=0:o=0:p=0:q=0:r=0
23 s=0:t=0:u=0:v=0:w=0:z=0
30 t=val(right$(time$,2))
40 for i=0 to 2000
50 x%=x%+1:y%=y%+1
60 next i
100 t1=val(right$(time$,2))
110 if t1<t then t1=t1+60
120 print t1-t
130 '25.0
<ADD3.BAS>
10 width 80,20
20 cls
22 a=0:b=0:c=0:d=0:e=0:f=0:g=0:h=0:j=0:k=0:l=0:m=0:o=0:p=0:q=0:r=0
23 s=0:t=0:u=0:v=0:w=0:z=0
25 x%=0:y%=0
30 t=val(right$(time$,2))
40 for i=0 to 2000
50 x%=x%+1:y%=y%+1
60 next i
100 t1=val(right$(time$,2))
110 if t1<t then t1=t1+60
120 print t1-t
130 '29.3

無駄な文を(たとえREM文でも)ループ内にいれるな]
ループ内に無駄な文をいれないなんてのは、当たり前の話ですが、非実行文のREM文でさえ、大幅な
スピードダウンにつながります。ADD2では55行に無駄なREM文をいれてみました。非実行文だから
スピードには関係ないだろう、という感じもしますが、結果は29.0秒と大幅なスピードダウウンです。

一般にBASICインタプリタはリンクポインタ(次に実行すべき行の先頭アドレスをさすポインタ)を順々に
たどって、ひとつひとつの行を実行していきます。
従って、55行が非実行文だからといって、そこを飛ばして50行から60行に実行がうつるわけではなく、
    50行終了
−−>55行のリンクポインタをみて55行の先頭に飛ぶ    
<・・・・無駄な作業
−−>REM文だ!と解釈する=以下、無視していい      
<・・・これも無駄 
−−>そんじゃ60行のリンクポインタをみて、60行の先頭へとぼう
と、無駄な作業が入ります。若干でもBASICインタプリタが無駄な作業をする分遅くなるわけです。
N88互換BASIC for WIN でも事情は同じようであると推測します。

コメントは適宜いれた方が、プログラムが見やすくなりますが、ループ内にいれるのはスピードの大幅な
犠牲を伴います。

* リンクポインタについては「(!)パソコン活用研究ラピュタへの道
/カセットインターフェースによるデータ
に解説ありますので参考にして下さい。

<ADD2.BAS>
10 width 80,20
20 cls
25 x%=0:y%=0
30 t=val(right$(time$,2))
40 for i=0 to 2000
50 x%=x%+1:y%=y%+1
55 REM  ******無駄なコメント行だよん*******************
60 next i
100 t1=val(right$(time$,2))
110 if t1<t then t1=t1+60
120 print t1-t
130 '29.0

[配列の宣言,定義はスピードに影響なし]
ADD4はADD3に加え、X%,Y%の定義の前に配列の定義を(1000個も)したものですが、
ADD3と結果は変わりません。

一般にBASICインタプリタでは配列と変数をそれぞれ別の領域に格納するので、どでかい配列の定義を
しても、変数サーチのスピードには影響ないものと思われます。この事情はN88互換BASIC for WINでも
また同じと思われます。

<ADD4.BAS>
10 width 80,20
20 cls
21 dim abc(1000)
22 a=0:b=0:c=0:d=0:e=0:f=0:g=0:h=0:j=0:k=0:l=0:m=0:o=0:p=0:q=0:r=0
23 s=0:t=0:u=0:v=0:w=0:z=0
24 for i=0 to 999: abc(i)=0:next
25 x%=0:y%=0
30 t=val(right$(time$,2))
40 for i=0 to 2000
50 x%=x%+1:y%=y%+1
60 next i
100 t1=val(right$(time$,2))
110 if t1<t then t1=t1+60
120 print t1-t
130 '29.3

[実数型演算でもスピード変わらず]
こんどは変数X,Yを実数型として、演算してみました。昔の常識では、実数型の演算は整数型より遅い、
ということでしたが、ADD5は24.6秒と基本形のADD0と変りありません。
これは、できるだけ変数は整数型にしてスピードアップをはかるのが定石、と思っている古いBASIC
プログラマ(おじさんも含めて)には少々ショッキングな結果ではないでしょうか。

昔の8ビットCPUでは、整数の加算減算はマシン語の命令があり、超高速でできましたが、除算あたりになると
自分で割り算用のルーチンをつくらねばならず、だいぶスピードがおちたものです。まして、実数の演算となると、浮動小数点方式とかいうのを使って、数十行以上のコードを書かねばならず、整数の演算に比べて格段
にスピードがおちたものです。
ところが、486以降のCPUでは、浮動小数点の計算がCPUに組み込まれ、大幅にスピードアップされました。
この結果はまさしくCPUの進化の反映であると思われます。

<ADD5.BAS>
10 width 80,20
20 cls
25 x=0:y=0
30 t=val(right$(time$,2))
40 for i=0 to 2000
50 x=x+1:y=y+1
60 next i
100 t1=val(right$(time$,2))
110 if t1<t then t1=t1+60
120 print t1-t
130 '24.6

[繰り返し処理はFOR・・・・・NEXTが速い]
繰り返し処理のループを作るのに、WHILE・・・・・WND や IF文を使うという手もありますが、これらの
条件判断を伴う命令は結構、時間を食います。あらかじめループ回数がわかっていて、FOR・・・・NEXT
が使えるなら、ループはFOR・・・NEXTでやるのがBESTです。
ADD7ではWHILE・・・WENDで同じ事をやってますが、32.6秒とかなりスピードダウンです。
ADD8はIF とGOTO の組み合わせでループを形成していますが、36.6秒と基本形ADD0の1.5倍の
時間がかかっています。

<ADD7.BAS>
10 width 80,20
20 cls
25 x%=0:y%=0
30 t=val(right$(time$,2))
40 while x%<=2000
50 x%=x%+1:y%=y%+1
60 wend
100 t1=val(right$(time$,2))
110 if t1<t then t1=t1+60
120 print t1-t
130 ' 32.6

<ADD8.BAS>
10 width 80,20
20 cls
25 x%=0:y%=0
30 t=val(right$(time$,2))
40 if x%>2000 then goto 100
50 x%=x%+1:y%=y%+1
60 goto 40
100 t1=val(right$(time$,2))
110 if t1<t then t1=t1+60
120 print t1-t
130 ' 36.6


(2) QBASICで実験
QBASICにおいても上のN88互換BASIC for WINの結果がほぼ適用できますが、整数型と実数型の
問題に関しては別の結果がでています。QBASICはMS−DOS用に作られたアプリケーションなので
(=CPUは8086を想定しているので実数型の演算は、CPUの浮動小数点用の演算命令は使わず、
マシン語で浮動小数点方式の演算ルーチンを組んで実行している)、整数型の演算のほうが高速に実行
されます。

ADD0が整数型の変数での演算、ADD1は実数型です。30000回の加算のループを4回くりかえして
ますが、ADD0では5秒、ADD1では13秒と、整数型のほうが圧倒的に高速という結果になりました。
QBASICでは、整数型の演算は実数型よりかなり高速ということになります。

<ADD0.BAS>
 WIDTH 80, 25
 CLS
 t = VAL(RIGHT$(TIME$, 2))
 FOR j = 0 TO 3
 x% = 0: y% = 0
 FOR i = 0 TO 30000
 x% = x% + 1: y% = y% + 1
 NEXT i: NEXT j
 t1 = VAL(RIGHT$(TIME$, 2))
 IF t1 < t THEN t1 = t1 + 60
 PRINT t1 - t
 ' 5
<ADD1.BAS>
 WIDTH 80, 25
 CLS
 t = VAL(RIGHT$(TIME$, 2))
 FOR j = 0 TO 3
 x = 0: y = 0
 FOR i = 0 TO 30000
 x = x + 1: y = y + 1
 NEXT i: NEXT j
 t1 = VAL(RIGHT$(TIME$, 2))
 IF t1 < t THEN t1 = t1 + 60
 PRINT t1 - t
 ' 13

2  GOTO と GOSUBによる分岐
GOTOやGOSUBの高速化の技というと、いったい何でしょうか。ある分岐先へ飛ぶだけだから、どこに
高速化する余地があるのか?という感じですが、ここにもBASICインタプリタの仕様から考えた高速化の
法則があります。

一般にBASICインタプリタはGOTO や GOSUB に出くわすと、次のように分岐先をサーチします。
a) 前の行に飛ぶ場合 −−>プログラム先頭行から順に、目的の行をサーチしていきます
b) 後ろの行に飛ぶ場合 −−>現在の行の次から目的の行をサーチしていきます

従って、GOTOの飛び先、GOSUBから飛ぶサブルーチンはできるだけプログラムの先頭の方にまとめる、
あるいは後ろに飛ばすなら、できるだけ直後に飛ばす、というのが高速化のための基本法則、と言えるでしょう。

下の実験はN88互換BASIC for WINでのものです。GOTO0では5から39行目まで無駄なREM文をいれて
みました。GOTO1では余分な行をプログラムの先頭から極力排除しました。(プログラムの初期化もいったん
後ろに飛ばして処理)。その結果、GOTO0で32秒、GOTO1で31秒とわずか1秒ですがGOTO0で余計に
時間がかかっています。

徹底してプログラムの高速化を図るなら、GOTO1のようにプログラムの初期設定、初期化の処理を、いったん
プログラムの末尾に持っていく、というのが定石といえるでしょう。

 

<GOTO0.BAS>
1 width 80,20
2 cls
3 x%=0
4 t=val(right$(time$,2))
5 rem
6 rem
7 rem
8 rem
9 rem
10 rem
11 rem
12 rem
13 rem
14 rem
15 rem
16 rem
17 rem
18 rem
19 rem
20 rem
21 rem
22 rem
23 rem
24 rem
25 rem
26 rem
27 rem
28 rem
29 rem
31 REM むだな行
32 rem
33 rem
34 rem
35 rem
36 rem
37 rem
38 rem
39 rem これも無駄な行
40 x%=x%+1
60 if x%>=2000 then goto 100 else goto 40
100 t1=val(right$(time$,2))
110 if t1<t then t1=t1+60
120 print t1-t
130 '32.0

<GOTO1.BAS>
10 goto 130
40 x%=x%+1
60 if x%>=2000 then goto 100 else goto 40
100 t1=val(right$(time$,2))
110 if t1<t then t1=t1+60
120 print t1-t:end
130 width 80,20
140 cls
150 x%=0
160 t=val(right$(time$,2))
170 goto 40
180 '31.0



というわけで、BASICプログラム高速化の方法をいくつかまとめてみました。

Top Page/ Basic活用研究目次