IF文を制する者はBASICを制す

PART 5 IF文の簡略化(実戦編)


 今までに等号による条件式は式を変形することで高速化ができたり、ANDやORを使った場合は分割して判定することで高速化ができるということを書いたわけですが、これがすべての場合において最善の方法というわけではありません。実際のプログラムにおいては別の方法で高速化ができる場合があったり、メモリ節約ができる場合もあります
 そこで私が商業誌への投稿プログラムで実際に使用したIF文を具体的に挙げそれについて解説していきます。

《例1》
M=N,M=VAL INKEY$:IF M-N THEN 150


 これは「パネルQ」キー入力で前回と同じキーを押していないかどうか(キーリピート防止)を判定しているものです。
 PART1PART2で書いた基本通り(M<>Nを減算にしただけ)なので詳しい説明は不要でしょう。これを使えば100m走などで使うキーの交互押し判定もさらに高速化できます。

《例2》
IF M+1AND SGN M LET K=M*0.5


 これは同じく「パネルQ」でパネルの移動のためのキー入力判定を行っています。上記の例1の続きでMは押したキーを示しています。(M=2→K=1、M=4→K=2、M=6→K=3、M=8→K=4、それ以外→K=0となるようにしている)
 では、これでなぜMの値が2、4、6、8の場合が判定できるかというとMが偶数だとM AND 1の値は0になりますが、これに1を足すことで奇数になるためにM+1 AND 1の値が1になり言い換えれば「0以外」となるためにTHEN以下が実行されるからです。

 これを分かりやすいIF文で書くと次のようになります。

 IF M=2 OR M=4 OR M=6 OR M=8 LET K=M*0.5

ORを使った複数の条件式は分けることで高速化が可能とPART4で書きましたがそれ行えば次のようになります。

 IF M-2 IF M-4 IF M-6 IF M-8 THEN ELSE K=M*0.5 ELSE K=M*0.5 ELSE K=M*0.5 ELSE K=M*0.5

 4つに分離することでELSE以下も4つ必要になるため見にくさやメモリ使用量増というデメリットがあって高速化が非常に重要という場合を除きあまり良いIF文とはいえないですね。しかも、高速化が可能といっても[2][4][6][8]それぞれのキーが押される確率を考慮するとそれほど高速化になっているというわけでもありまん。例2の最初に挙げたものは理論上の最速ではないもののORを3つ使うよりは5.1m秒速く、メモリも節約できるということでバランス面で優れていると思います。

 ちなみにこのIF文は次のように置き換えることも可能です。

 IF M/2=INT (M/2) IF M>0 LET K=M*0.5

 これは「2で割ったものが2で割ったものを整数化したものと等しい」(つまり、2で割り切れる)ということで偶数かどうかを判定するIF文となります。「偶数ならば〜」という条件で判定をする場合は大半の人が最初にこのIF文を思い浮かべると思いますが、PC-E500シリーズの場合はこちらの方が24バイト余分にメモリが必要になるうえに速度が6m秒も遅くなってしまうためにこれがベストだと感じる人はいないでしょう。

《例3》
IF INT (X*0.0064)LET X=SGN X*78+78


これは「シュプール」において、プレイヤーが指定範囲(X座標が0から156までの範囲)からはみ出ないように判定しているもので分かりやすいIF文で書くと次のようになります。

 IF X<0 LET X=0
 IF X>156 LET X=156


 この2つのIF文を強引に1つにまとめたわけですが、これができる理由は非常に簡単です。
 条件式のINT (X*0.0064)ですが、この式の値はX<0の時は-1となり、X>=156.25の時は1となります。PART1で書いたようにIF文では条件式の部分が0か0以外かで判断しているということを利用して条件式を簡略化しているのです。
 これを見るとほぼ同じ機能を持つというのが分かると思いますが、X座標の大きい方に0.25の誤差がありますね。これを回避するには単純にINT (X/156)とすれば良いのですが、除算よりも乗算の方が速いし、PC-E500シリーズは桁数に関係なく単精度の数値は8バイトとして扱われるため見た目の文字数が変化しても使用メモリは変わりません
 あと、このゲームの場合X座標の上限範囲が0.25ドットずれても影響を与えないために速い方が有利となります。LET以下の処理が複雑化していますが、X座標の範囲はかなり大きくとっているために通常にプレイをしていればこの範囲から外れることは滅多になくLET以下が実行されることはほとんどありません。したがって、平均速度を計算すれば無視が可能となるわけです。
 べき乗演算の高速化OMPのところで書いたように自分のプログラムでどの程度の演算精度が必要か把握しておくことでその誤差を有効活用することで高速化が可能になる場合があります

コラム 演算誤差を考慮した高速化

 数値演算分野においては必要とする有効桁数によって必要なループ回数が決まってくるために無駄に計算をしないことが高速化に繋がってくる。しかし、BASICでゲーム製作などをする場合においてそれが重要になることは少ない。
 ただ、ポケコンではなく一般的なパソコン用BASICの場合基本的に扱える数値は(内部的には)二進数であるためにこの誤差が重要になってくる。例えば0.1は二進数では循環小数になってしまうためにIF X=0.1 THEN 30とかやってしまうと誤差のため正常動作しなくなる場合がある
 電卓の発展系であるポケコンの場合は内部でも十進数で演算しておりそういう誤差は発生しないため演算誤差を考慮するという機会はなかなかない。したがって、誤差を利用して高速化が可能になるのは上記の例のように除算の代わりに乗算を使う場合などごく一部の場合に限られる。
 あと誤差の活用場所として有用なのは多くの命令が引数は整数部分のみ有効というものだろう。例えばBEEPでは整数部分のみ有効なのでOMPではそれを利用して計算式を簡略化することで高速化している(演算結果の誤差がプラス1未満であれば問題ないため)。これはLOCATEなどにおいてもいえるためPRINTでドット単位に表示する場合の80行のLOCATE X/6,2LOCATE X*0.167,2としても問題ない。ただ、誤差は積み重なれば拡大するため本当に問題がないレベルかは実際に検証しておく必要がある。ちなみに上記OMPではすべての場合において問題がないことを確かめた後に発表した。

《例4》
IF RS*2+(A<103)+(BA<103)LET MS=RS ELSE MS=1-RS


これは「ピーチバレー」において、1Pと2Pのどちらがミスしたかを判定するものです。(X座標103がコートの中央部分となり、それより小さい方が1P側、それより大きい方が2P側のコートとなる)
変数 AボールのX座標
変数 BA最初にボールが落下したX座標
変数 RS最後にレシーブしたプレイヤー(0:1P、1:2P)
変数 MSミスしたプレイヤー(1:1P、0:2P)

 このゲームはボールが地面に落下してもレシーブ(サーブ、スパイクも含む)したボールは自然に止まるまで動き続けるため最終的なX座標でどちらのミスかは判断ができません。さらに一旦相手のコートに落下したボールが自分のコートに戻ってくるなどの例外が起こるため最初に落下したX座標でも判断することができません。

上記4つの変数を元に1Pのミスを判定する場合は下記の3パターンが必要になります。
1Pがレシーブして1P側のコートに落下した場合(最終位置はどこでもOK)
1Pがレシーブして2P側のコートに落下し、最終位置が1P側のコートにある場合(ネットで反射)
2Pがレシーブして1P側のコートに落下し、最終位置が1P側のコートにある場合
2Pのミスとなるのはこの逆のパターンで同じく3通りとなり普通に判定すれば合計6つのIF文が必要になります。

 IF RS=0 AND BA<103 LET MS=1
 IF RS=0 AND BA>=103 AND A<103 LET MS=1
 IF RS=1 AND BA<103 AND A<103 MS=1
 IF RS=1 AND BA>=103 LET MS=0
 IF RS=1 AND BA<103 AND A>=103 LET MS=0
 IF RS=0 AND BA>=103 AND A>=103 MS=0


こんな複雑なことを考えなくてもボールがネットの上を通過したかという判定を1つ入れればかなり楽になるわけですが、その判定は常時必要になるため速度の大きな低下を招いてしまいます。判定方法を変えることで実際に速くなるかは例3で書いたように判定を行う確率(頻度)も考慮する必要がありメインルーチン1回ごとに1回判定が必要になるのとミスしたときのみ1回判定が必要になるのでは平均実行速度を考えた場合には大きな差となって表れてきますからね。

 実は8方向移動判定のところでも書いたようにパターン化されたIF文の羅列は簡略化可能であるためこれも簡略化が可能になります。上記で長々と書いたミス判定も視点を変えれば「1P、2P共通で相手のコートに落下して最終位置が相手のコートにあれば相手のミス、そうでなければ自分のミスとなる」という感じでかなり簡略化されます。

これをIF文で書くと次のようになります。
1Pがレシーブの場合 IF RS=0 AND A>=103 AND BA>=103 LET MS=0 ELSE MS=1
2Pがレシーブの場合 IF RS=1 AND A<103 AND BA<103 LET MS=1 ELSE MS=0

 単に判定基準が「どんな場合に1Pのミス、もしくは2Pのミスになる<のか」というものだったのを「どんな場合にレシーブした側がミスになるのか」というものに変えただけです。ある程度の経験があればいきなりこの形にたどり着くようになるために最初に提示した1P3通り、2P3通り、計6通りのIF文で判定するというのを冗長に感じた人も多いのではないかと思います。
 この2つのIF文を1つのIF文にまとめると例4の最初の形になります。これもIF文が0か0以外かで判定するということを利用することによって1つのIF文にまとめることが可能になっています。

 ちなみにRSとMSで1P、2Pを示す値が異なるのはその方がこのIF文だけではなくプログラム全体で考えた場合においてトータルで4バイト節約できるからです。ここまで書いてボールがコートアウトになった場合の処理がないことに気づいた人もいるかもしれませんが、これはコート内にボールが落ちるのとは別に処理をした方がメモリを節約できるということで今回は省いています。
 ボールの落下位置がコート内かコート外かを判断し、コート外であればアウトになり、上記ミス判定における1Pと2Pの値を逆にすればいいだけですからね。ミス判定とアウト判定を1つのIFにまとめることは無理ではないけど無駄が多すぎるためIF文の数(使用行数)を単純に減らせば良いというものではありません。

 このように全く同じものを判定するにもその方法は無数に存在しますが、その中から処理速度だけでなく使用メモリやプログラムリストの見やすさなどを考え何をを重視するかで最も適するIF文はどれなのかが決まります。どのようなIF文であっても条件判断の基本はPART1で書いたように条件式の値が0か0以外かで判断しているので簡略化で迷った場合はまずは原点に返ることも重要となるでしょう。しかし、そのIF文だけではなくプログラムリスト内の他の部分との関わりを考えることでさらなる高速化や省メモリ化が可能になってくる場合もあるため単純にその行だけ考えれば良いというものではなく全体を把握しておかなくてはなりません

PART6に続く


PART 1 IF〜THEN〜の動作の基本について
PART 2 条件式の簡略化(基本編)
PART 3 IF文は論理式よりも速い!?
PART 4 IF文の簡略化(応用編)
PART 5 IF文の簡略化(実戦編)
PART 6 IF文を制するには・・・

RETURN

inserted by FC2 system